diff --git a/src/Framework/ChangeWaves.cs b/src/Framework/ChangeWaves.cs index 52ab8929eeb..301873a3182 100644 --- a/src/Framework/ChangeWaves.cs +++ b/src/Framework/ChangeWaves.cs @@ -35,7 +35,8 @@ internal static class ChangeWaves internal static readonly Version Wave18_5 = new Version(18, 5); internal static readonly Version Wave18_6 = new Version(18, 6); internal static readonly Version Wave18_7 = new Version(18, 7); - internal static readonly Version[] AllWaves = [Wave17_10, Wave17_12, Wave17_14, Wave18_3, Wave18_4, Wave18_5, Wave18_6, Wave18_7]; + internal static readonly Version Wave18_8 = new Version(18, 8); + internal static readonly Version[] AllWaves = [Wave17_10, Wave17_12, Wave17_14, Wave18_3, Wave18_4, Wave18_5, Wave18_6, Wave18_7, Wave18_8]; /// /// Special value indicating that all features behind all Change Waves should be enabled. diff --git a/src/Tasks.UnitTests/AssemblyDependency/AssemblyFoldersFromConfig_Tests.cs b/src/Tasks.UnitTests/AssemblyDependency/AssemblyFoldersFromConfig_Tests.cs index e493b7d9b11..53a96f9cb58 100644 --- a/src/Tasks.UnitTests/AssemblyDependency/AssemblyFoldersFromConfig_Tests.cs +++ b/src/Tasks.UnitTests/AssemblyDependency/AssemblyFoldersFromConfig_Tests.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.IO; diff --git a/src/Tasks.UnitTests/AssemblyDependency/GlobalAssemblyCacheTests.cs b/src/Tasks.UnitTests/AssemblyDependency/GlobalAssemblyCacheTests.cs index b08a9c21a22..77115c7cf15 100644 --- a/src/Tasks.UnitTests/AssemblyDependency/GlobalAssemblyCacheTests.cs +++ b/src/Tasks.UnitTests/AssemblyDependency/GlobalAssemblyCacheTests.cs @@ -56,7 +56,7 @@ public void VerifySimpleNamev2057020() 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); + string path = GlobalAssemblyCache.GetLocation(fusionName, SystemProcessorArchitecture.None, _runtimeVersion, new Version("2.0.57027"), false, new FileExists(MockFileExists), _getPathFromFusionName, _gacEnumerator, false, TaskEnvironmentHelper.CreateForTest()); Assert.NotNull(path); Assert.Equal(system2Path, path); } @@ -81,7 +81,7 @@ public void VerifySimpleNamev2057020SpecificVersion() 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); + string path = GlobalAssemblyCache.GetLocation(fusionName, SystemProcessorArchitecture.None, _runtimeVersion, new Version("2.0.0"), false, new FileExists(MockFileExists), _getPathFromFusionName, _gacEnumerator, true, TaskEnvironmentHelper.CreateForTest()); Assert.NotNull(path); Assert.Equal(system4Path, path); } @@ -104,7 +104,7 @@ public void VerifyFusionNamev2057020SpecificVersion() 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); + string path = GlobalAssemblyCache.GetLocation(fusionName, SystemProcessorArchitecture.None, _runtimeVersion, new Version("2.0.0"), false, new FileExists(MockFileExists), _getPathFromFusionName, _gacEnumerator, true, TaskEnvironmentHelper.CreateForTest()); Assert.NotNull(path); Assert.Equal(system2Path, path); } @@ -127,7 +127,7 @@ public void VerifySimpleNamev40() 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); + string path = GlobalAssemblyCache.GetLocation(fusionName, SystemProcessorArchitecture.None, _runtimeVersion, new Version("4.0.0"), false, new FileExists(MockFileExists), _getPathFromFusionName, _gacEnumerator, false, TaskEnvironmentHelper.CreateForTest()); Assert.NotNull(path); Assert.Equal(system4Path, path); } @@ -151,7 +151,7 @@ public void VerifySimpleNamev40SpecificVersion() 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); + string path = GlobalAssemblyCache.GetLocation(fusionName, SystemProcessorArchitecture.None, _runtimeVersion, new Version("4.0.0"), false, new FileExists(MockFileExists), _getPathFromFusionName, _gacEnumerator, true, TaskEnvironmentHelper.CreateForTest()); Assert.NotNull(path); Assert.Equal(system4Path, path); } @@ -172,7 +172,7 @@ public void VerifyFusionNamev40SpecificVersion() 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); + string path = GlobalAssemblyCache.GetLocation(fusionName, SystemProcessorArchitecture.None, _runtimeVersion, new Version("4.0.0.0"), false, new FileExists(MockFileExists), _getPathFromFusionName, _gacEnumerator, true, TaskEnvironmentHelper.CreateForTest()); Assert.NotNull(path); Assert.Equal(system4Path, path); } @@ -186,7 +186,7 @@ public void VerifyEmptyPublicKeyspecificVersion() Assert.Throws(() => { 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); + string path = GlobalAssemblyCache.GetLocation(fusionName, SystemProcessorArchitecture.None, getRuntimeVersion, new Version("2.0.50727"), false, new FileExists(MockFileExists), _getPathFromFusionName, _gacEnumerator, true, TaskEnvironmentHelper.CreateForTest()); }); } @@ -197,7 +197,7 @@ public void VerifyEmptyPublicKeyspecificVersion() 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); + string path = GlobalAssemblyCache.GetLocation(fusionName, SystemProcessorArchitecture.None, getRuntimeVersion, new Version("2.0.50727"), false, new FileExists(MockFileExists), _getPathFromFusionName, _gacEnumerator, false, TaskEnvironmentHelper.CreateForTest()); Assert.Null(path); } @@ -208,7 +208,7 @@ public void VerifyNullPublicKey() 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); + string path = GlobalAssemblyCache.GetLocation(fusionName, SystemProcessorArchitecture.None, getRuntimeVersion, new Version("2.0.50727"), false, new FileExists(MockFileExists), _getPathFromFusionName, _gacEnumerator, true, TaskEnvironmentHelper.CreateForTest()); Assert.Null(path); } @@ -221,7 +221,7 @@ public void VerifyNullPublicKeyspecificVersion() 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); + 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, TaskEnvironmentHelper.CreateForTest()); Assert.Null(path); } @@ -233,7 +233,7 @@ public void VerifyProcessorArchitectureDoesNotCrash() 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); + 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, TaskEnvironmentHelper.CreateForTest()); Assert.Null(path); } @@ -245,7 +245,7 @@ public void VerifyProcessorArchitectureDoesNotCrashSpecificVersion() 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); + 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, TaskEnvironmentHelper.CreateForTest()); Assert.Null(path); } @@ -257,7 +257,7 @@ public void VerifyProcessorArchitectureDoesNotCrashFullFusionName() 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); + 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, TaskEnvironmentHelper.CreateForTest()); Assert.Null(path); } @@ -271,6 +271,7 @@ public void SystemRuntimeDepends_No_Build() ResolveAssemblyReference t = new ResolveAssemblyReference(); t.BuildEngine = new MockEngine(_output); + t.TaskEnvironment = TaskEnvironmentHelper.CreateForTest(); t.Assemblies = new ITaskItem[] { @@ -340,6 +341,7 @@ public void SystemRuntimeDepends_Yes() ResolveAssemblyReference t = new ResolveAssemblyReference(); t.BuildEngine = new MockEngine(_output); + t.TaskEnvironment = TaskEnvironmentHelper.CreateForTest(); t.Assemblies = new ITaskItem[] { @@ -407,6 +409,7 @@ public void SystemRuntimeDepends_Yes_Indirect() ResolveAssemblyReference t = new ResolveAssemblyReference(); t.BuildEngine = new MockEngine(_output); + t.TaskEnvironment = TaskEnvironmentHelper.CreateForTest(); t.Assemblies = new ITaskItem[] { @@ -473,6 +476,7 @@ public void SystemRuntimeDepends_Yes_Indirect_ExternallyResolved() ResolveAssemblyReference t = new ResolveAssemblyReference(); t.BuildEngine = new MockEngine(_output); + t.TaskEnvironment = TaskEnvironmentHelper.CreateForTest(); t.Assemblies = new ITaskItem[] { @@ -537,6 +541,7 @@ public void NETStandardDepends_Yes() ResolveAssemblyReference t = new ResolveAssemblyReference(); t.BuildEngine = new MockEngine(_output); + t.TaskEnvironment = TaskEnvironmentHelper.CreateForTest(); t.Assemblies = new ITaskItem[] { @@ -601,6 +606,7 @@ public void NETStandardDepends_Yes_Indirect() ResolveAssemblyReference t = new ResolveAssemblyReference(); t.BuildEngine = new MockEngine(_output); + t.TaskEnvironment = TaskEnvironmentHelper.CreateForTest(); t.Assemblies = new ITaskItem[] { @@ -666,6 +672,7 @@ public void NETStandardDepends_Yes_Indirect_ExternallyResolved() ResolveAssemblyReference t = new ResolveAssemblyReference(); t.BuildEngine = new MockEngine(_output); + t.TaskEnvironment = TaskEnvironmentHelper.CreateForTest(); t.Assemblies = new ITaskItem[] { @@ -731,6 +738,7 @@ public void DependsOn_NETStandard_and_SystemRuntime() ResolveAssemblyReference t = new ResolveAssemblyReference(); t.BuildEngine = new MockEngine(_output); + t.TaskEnvironment = TaskEnvironmentHelper.CreateForTest(); t.Assemblies = new ITaskItem[] { @@ -799,6 +807,7 @@ public void DependsOn_NETStandard_and_SystemRuntime_ExternallyResolved() ResolveAssemblyReference t = new ResolveAssemblyReference(); t.BuildEngine = new MockEngine(_output); + t.TaskEnvironment = TaskEnvironmentHelper.CreateForTest(); t.Assemblies = new ITaskItem[] { diff --git a/src/Tasks.UnitTests/AssemblyDependency/Miscellaneous.cs b/src/Tasks.UnitTests/AssemblyDependency/Miscellaneous.cs index 32a57278a7b..68fa4bea1d1 100644 --- a/src/Tasks.UnitTests/AssemblyDependency/Miscellaneous.cs +++ b/src/Tasks.UnitTests/AssemblyDependency/Miscellaneous.cs @@ -1135,23 +1135,56 @@ public void Regress286699_InvalidSearchPath() } /// - /// Invalid app.config path should not crash. + /// Invalid or empty app.config paths should not crash. + /// Invalid path "|" causes a logged error and task failure. + /// Empty string is silently ignored (Wave18_8 behavior) and task succeeds. /// - [Fact] - public void Regress286699_InvalidAppConfig() + [Theory] + [InlineData("|", false)] + [InlineData("", true)] + public void InvalidOrEmptyAppConfig_DoesNotCrash(string appConfigFile, bool expectedSuccess) { ResolveAssemblyReference t = new ResolveAssemblyReference(); t.BuildEngine = new MockEngine(_output); - - t.Assemblies = new ITaskItem[] { new TaskItem("mscorlib") }; - t.AppConfigFile = "|"; + t.Assemblies = [new TaskItem("mscorlib")]; + t.AppConfigFile = appConfigFile; bool retval = Execute(t); - Assert.False(retval); + retval.ShouldBe(expectedSuccess); + } - // Should not crash. + /// + /// When Wave18_8 is disabled, empty AppConfigFile should cause the task to fail + /// with an error, preserving backward-compatible behavior. + /// + [Fact] + public void EmptyAppConfigFile_Wave18_8_Disabled_Fails() + { + try + { + using TestEnvironment env = TestEnvironment.Create(_output); + + ChangeWaves.ResetStateForTests(); + env.SetEnvironmentVariable("MSBUILDDISABLEFEATURESFROMVERSION", ChangeWaves.Wave18_8.ToString()); + BuildEnvironmentHelper.ResetInstance_ForUnitTestsOnly(); + + ResolveAssemblyReference t = new ResolveAssemblyReference(); + + MockEngine engine = new MockEngine(_output); + t.BuildEngine = engine; + t.Assemblies = new ITaskItem[] { new TaskItem("mscorlib") }; + t.AppConfigFile = string.Empty; + + bool retval = Execute(t); + retval.ShouldBeFalse(); + engine.Errors.ShouldBe(1); + } + finally + { + ChangeWaves.ResetStateForTests(); + } } /// @@ -6702,7 +6735,7 @@ public void ReferenceTableDependentItemsInDenyList4() #if FEATURE_WIN32_REGISTRY null, null, null, #endif - null, null, null, new Version("4.0"), null, null, null, true, false, null, null, false, null, WarnOrErrorOnTargetArchitectureMismatchBehavior.None, false, false, null, Array.Empty()); + null, null, null, new Version("4.0"), null, null, null, true, false, null, null, false, null, WarnOrErrorOnTargetArchitectureMismatchBehavior.None, false, false, null, Array.Empty(), TaskEnvironmentHelper.CreateForTest()); MockEngine mockEngine; ResolveAssemblyReference rar; Dictionary denyList; @@ -6880,7 +6913,7 @@ private static ReferenceTable MakeEmptyReferenceTable(TaskLoggingHelper log) #if FEATURE_WIN32_REGISTRY null, null, null, #endif - null, null, new Version("4.0"), null, log, null, true, false, null, null, false, null, WarnOrErrorOnTargetArchitectureMismatchBehavior.None, false, false, null, Array.Empty()); + null, null, new Version("4.0"), null, log, null, true, false, null, null, false, null, WarnOrErrorOnTargetArchitectureMismatchBehavior.None, false, false, null, Array.Empty(), TaskEnvironmentHelper.CreateForTest()); return referenceTable; } diff --git a/src/Tasks.UnitTests/AssemblyDependency/Node/RarNodeExecuteRequest_Tests.cs b/src/Tasks.UnitTests/AssemblyDependency/Node/RarNodeExecuteRequest_Tests.cs index c2e2a2a55d2..cc80fbbf105 100644 --- a/src/Tasks.UnitTests/AssemblyDependency/Node/RarNodeExecuteRequest_Tests.cs +++ b/src/Tasks.UnitTests/AssemblyDependency/Node/RarNodeExecuteRequest_Tests.cs @@ -57,26 +57,6 @@ public void TaskInputsArePropagated() Assert.Equal(clientRar.StateFile, nodeRar.StateFile); } - [Fact] - public void KnownRelativePathsAreResolvedToFullPaths() - { - const string AppConfigFileName = "App.config"; - const string StateFileName = "AssemblyReference.cache"; - ResolveAssemblyReference clientRar = new() - { - BuildEngine = new MockEngine(), - AppConfigFile = AppConfigFileName, - StateFile = StateFileName, - }; - RarNodeExecuteRequest request = new(clientRar); - - ResolveAssemblyReference nodeRar = new(); - request.SetTaskInputs(nodeRar, CreateBuildEngine()); - - Assert.Equal(Path.GetFullPath(AppConfigFileName), nodeRar.AppConfigFile); - Assert.Equal(Path.GetFullPath(StateFileName), nodeRar.StateFile); - } - [Fact] public void BuildEngineSettingsArePropagated() { diff --git a/src/Tasks.UnitTests/AssemblyDependency/ResolveAssemblyReferenceTestFixture.cs b/src/Tasks.UnitTests/AssemblyDependency/ResolveAssemblyReferenceTestFixture.cs index ee85b41f383..e8bac774e59 100644 --- a/src/Tasks.UnitTests/AssemblyDependency/ResolveAssemblyReferenceTestFixture.cs +++ b/src/Tasks.UnitTests/AssemblyDependency/ResolveAssemblyReferenceTestFixture.cs @@ -809,7 +809,7 @@ private static string GetPathForAssemblyInGac(AssemblyNameExtension assemblyName #if FEATURE_GAC if (assemblyName.Version != null) { - gacLocation = GlobalAssemblyCache.GetLocation(assemblyName, targetProcessorArchitecture, getRuntimeVersion, targetedRuntimeVersion, fullFusionName, fileExists, null, null, specificVersion /* this value does not matter if we are passing a full fusion name*/); + gacLocation = GlobalAssemblyCache.GetLocation(assemblyName, targetProcessorArchitecture, getRuntimeVersion, targetedRuntimeVersion, fullFusionName, fileExists, null, null, specificVersion /* this value does not matter if we are passing a full fusion name*/, TaskEnvironmentHelper.CreateForTest()); } #endif return gacLocation; @@ -854,10 +854,8 @@ internal static bool FileExists(string path) return false; } - if (!Path.IsPathRooted(path)) - { - path = Path.GetFullPath(path); - } + // Canonicalize the path (resolve ".." segments etc.) so it matches s_existentFiles entries. + path = Path.GetFullPath(path); foreach (string file in s_existentFiles) { @@ -1048,10 +1046,8 @@ internal static AssemblyNameExtension GetAssemblyName(string path) throw new FileNotFoundException(path); } - if (!Path.IsPathRooted(path)) - { - path = Path.GetFullPath(path); - } + // Canonicalize the path (resolve ".." segments etc.) so it matches expected entries. + path = Path.GetFullPath(path); if ( diff --git a/src/Tasks.UnitTests/AssemblyDependency/VerifyTargetFrameworkAttribute.cs b/src/Tasks.UnitTests/AssemblyDependency/VerifyTargetFrameworkAttribute.cs index ced2b80ac2a..a3805d2a22d 100644 --- a/src/Tasks.UnitTests/AssemblyDependency/VerifyTargetFrameworkAttribute.cs +++ b/src/Tasks.UnitTests/AssemblyDependency/VerifyTargetFrameworkAttribute.cs @@ -33,6 +33,7 @@ public void FrameworksDoNotMatch() new TaskItem("DependsOnFoo4Framework"), }; + ResolveAssemblyReference t = new ResolveAssemblyReference(); t.BuildEngine = e; t.Assemblies = items; diff --git a/src/Tasks.UnitTests/HintPathResolver_Tests.cs b/src/Tasks.UnitTests/HintPathResolver_Tests.cs index a40170b3152..59298eb9e2a 100644 --- a/src/Tasks.UnitTests/HintPathResolver_Tests.cs +++ b/src/Tasks.UnitTests/HintPathResolver_Tests.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; @@ -71,7 +71,8 @@ private bool ResolveHintPath(string hintPath) getAssemblyName: (path) => throw new NotImplementedException(), // not called in this code path fileExists: p => FileUtilities.FileExistsNoThrow(p), getRuntimeVersion: (path) => throw new NotImplementedException(), // not called in this code path - targetedRuntimeVesion: Version.Parse("4.0.30319")); + targetedRuntimeVesion: Version.Parse("4.0.30319"), + taskEnvironment: TaskEnvironmentHelper.CreateForTest()); var result = hintPathResolver.Resolve(new AssemblyNameExtension("FakeSystem.Net.Http"), sdkName: "", diff --git a/src/Tasks.UnitTests/RARPrecomputedCache_Tests.cs b/src/Tasks.UnitTests/RARPrecomputedCache_Tests.cs index d3c94688e18..19b6b7cb664 100644 --- a/src/Tasks.UnitTests/RARPrecomputedCache_Tests.cs +++ b/src/Tasks.UnitTests/RARPrecomputedCache_Tests.cs @@ -87,7 +87,9 @@ public void StandardCacheTakesPrecedence() // Write precomputed cache rarWriterTask.WriteStateFile(); - ResolveAssemblyReference rarReaderTask = new ResolveAssemblyReference(); + ResolveAssemblyReference rarReaderTask = new ResolveAssemblyReference() + { + }; rarReaderTask.StateFile = standardCache.Path; rarReaderTask.AssemblyInformationCachePaths = new ITaskItem[] { @@ -131,7 +133,9 @@ public void TestPreComputedCacheInputMatchesOutput() File.Delete(precomputedCache.Path); rarWriterTask.WriteStateFile(); - ResolveAssemblyReference rarReaderTask = new ResolveAssemblyReference(); + ResolveAssemblyReference rarReaderTask = new ResolveAssemblyReference() + { + }; rarReaderTask.StateFile = precomputedCache.Path.Substring(0, precomputedCache.Path.Length - 6); // Not a real path; should not be used. rarReaderTask.AssemblyInformationCachePaths = new ITaskItem[] { diff --git a/src/Tasks/AssemblyDependency/AssemblyFoldersExResolver.cs b/src/Tasks/AssemblyDependency/AssemblyFoldersExResolver.cs index 49ba2302654..6919e577421 100644 --- a/src/Tasks/AssemblyDependency/AssemblyFoldersExResolver.cs +++ b/src/Tasks/AssemblyDependency/AssemblyFoldersExResolver.cs @@ -99,8 +99,8 @@ internal class AssemblyFoldersExResolver : Resolver /// /// Construct. /// - public AssemblyFoldersExResolver(string searchPathElement, GetAssemblyName getAssemblyName, FileExists fileExists, GetRegistrySubKeyNames getRegistrySubKeyNames, GetRegistrySubKeyDefaultValue getRegistrySubKeyDefaultValue, GetAssemblyRuntimeVersion getRuntimeVersion, OpenBaseKey openBaseKey, Version targetedRuntimeVesion, ProcessorArchitecture targetProcessorArchitecture, bool compareProcessorArchitecture, IBuildEngine buildEngine) - : base(searchPathElement, getAssemblyName, fileExists, getRuntimeVersion, targetedRuntimeVesion, targetProcessorArchitecture, compareProcessorArchitecture) + public AssemblyFoldersExResolver(string searchPathElement, GetAssemblyName getAssemblyName, FileExists fileExists, GetRegistrySubKeyNames getRegistrySubKeyNames, GetRegistrySubKeyDefaultValue getRegistrySubKeyDefaultValue, GetAssemblyRuntimeVersion getRuntimeVersion, OpenBaseKey openBaseKey, Version targetedRuntimeVesion, ProcessorArchitecture targetProcessorArchitecture, bool compareProcessorArchitecture, IBuildEngine buildEngine, TaskEnvironment taskEnvironment) + : base(searchPathElement, getAssemblyName, fileExists, getRuntimeVersion, targetedRuntimeVesion, targetProcessorArchitecture, compareProcessorArchitecture, taskEnvironment) { _buildEngine = buildEngine as IBuildEngine4; _getRegistrySubKeyNames = getRegistrySubKeyNames; @@ -161,7 +161,7 @@ private void LazyInitialize() } _wasMatch = true; - bool useCache = Environment.GetEnvironmentVariable("MSBUILDDISABLEASSEMBLYFOLDERSEXCACHE") == null; + bool useCache = taskEnvironment.GetEnvironmentVariable("MSBUILDDISABLEASSEMBLYFOLDERSEXCACHE") == null; string key = "ca22615d-aa83-444b-80b9-b32f3d5db097" + this.searchPathElement; if (useCache && _buildEngine != null) { @@ -171,7 +171,7 @@ private void LazyInitialize() if (_assemblyFoldersCache == null) { AssemblyFoldersEx assemblyFolders = new AssemblyFoldersEx(_registryKeyRoot, _targetRuntimeVersion, _registryKeySuffix, _osVersion, _platform, _getRegistrySubKeyNames, _getRegistrySubKeyDefaultValue, this.targetProcessorArchitecture, _openBaseKey); - _assemblyFoldersCache = new AssemblyFoldersExCache(assemblyFolders, fileExists); + _assemblyFoldersCache = new AssemblyFoldersExCache(assemblyFolders, fileExists, taskEnvironment); if (useCache) { _buildEngine?.RegisterTaskObject(key, _assemblyFoldersCache, RegisteredTaskObjectLifetime.Build, true /* dispose early ok*/); @@ -212,7 +212,10 @@ public override bool Resolve( { foreach (AssemblyFoldersExInfo assemblyFolder in _assemblyFoldersCache.AssemblyFoldersEx) { - string candidatePath = ResolveFromDirectory(assemblyName, isPrimaryProjectReference, wantSpecificVersion, executableExtensions, assemblyFolder.DirectoryPath, assembliesConsideredAndRejected); + // Defensively absolutize the directory path — it comes from the registry and should be absolute, + // but we enforce it rather than assume it. + string directoryPath = taskEnvironment.GetAbsolutePath(assemblyFolder.DirectoryPath).Value; + string candidatePath = ResolveFromDirectory(assemblyName, isPrimaryProjectReference, wantSpecificVersion, executableExtensions, directoryPath, assembliesConsideredAndRejected); // We have a full path returned if (candidatePath != null) @@ -280,12 +283,12 @@ internal class AssemblyFoldersExCache /// /// Constructor /// - internal AssemblyFoldersExCache(AssemblyFoldersEx assemblyFoldersEx, FileExists fileExists) + internal AssemblyFoldersExCache(AssemblyFoldersEx assemblyFoldersEx, FileExists fileExists, TaskEnvironment taskEnvironment) { AssemblyFoldersEx = assemblyFoldersEx; _fileExists = fileExists; - if (Environment.GetEnvironmentVariable("MSBUILDDISABLEASSEMBLYFOLDERSEXCACHE") != null) + if (taskEnvironment.GetEnvironmentVariable("MSBUILDDISABLEASSEMBLYFOLDERSEXCACHE") != null) { _useOriginalFileExists = true; } diff --git a/src/Tasks/AssemblyDependency/AssemblyFoldersFromConfig/AssemblyFoldersFromConfigCache.cs b/src/Tasks/AssemblyDependency/AssemblyFoldersFromConfig/AssemblyFoldersFromConfigCache.cs index 45f0d257c12..957f69b06c5 100644 --- a/src/Tasks/AssemblyDependency/AssemblyFoldersFromConfig/AssemblyFoldersFromConfigCache.cs +++ b/src/Tasks/AssemblyDependency/AssemblyFoldersFromConfig/AssemblyFoldersFromConfigCache.cs @@ -35,22 +35,25 @@ internal class AssemblyFoldersFromConfigCache /// /// Constructor /// - internal AssemblyFoldersFromConfigCache(AssemblyFoldersFromConfig assemblyFoldersFromConfig, FileExists fileExists) + internal AssemblyFoldersFromConfigCache(AssemblyFoldersFromConfig assemblyFoldersFromConfig, FileExists fileExists, TaskEnvironment taskEnvironment) { AssemblyFoldersFromConfig = assemblyFoldersFromConfig; _fileExists = fileExists; - if (Environment.GetEnvironmentVariable("MSBUILDDISABLEASSEMBLYFOLDERSEXCACHE") != null) + if (taskEnvironment.GetEnvironmentVariable("MSBUILDDISABLEASSEMBLYFOLDERSEXCACHE") != null) { _useOriginalFileExists = true; } else { + // Absolutize directory paths defensively — config paths theoretically should but may not be absolute. _filesInDirectories = new(assemblyFoldersFromConfig.AsParallel() - .Where(assemblyFolder => FileUtilities.DirectoryExistsNoThrow(assemblyFolder.DirectoryPath)) + .Where(assemblyFolder => !string.IsNullOrEmpty(assemblyFolder.DirectoryPath)) + .Select(assemblyFolder => taskEnvironment.GetAbsolutePath(assemblyFolder.DirectoryPath).Value) + .Where(absolutePath => FileUtilities.DirectoryExistsNoThrow(absolutePath)) .SelectMany( - assemblyFolder => - Directory.GetFiles(assemblyFolder.DirectoryPath, "*.*", SearchOption.TopDirectoryOnly)), + absolutePath => + Directory.GetFiles(absolutePath, "*.*", SearchOption.TopDirectoryOnly)), StringComparer.OrdinalIgnoreCase); } } diff --git a/src/Tasks/AssemblyDependency/AssemblyFoldersFromConfig/AssemblyFoldersFromConfigResolver.cs b/src/Tasks/AssemblyDependency/AssemblyFoldersFromConfig/AssemblyFoldersFromConfigResolver.cs index 4208cf4c72e..49427baf978 100644 --- a/src/Tasks/AssemblyDependency/AssemblyFoldersFromConfig/AssemblyFoldersFromConfigResolver.cs +++ b/src/Tasks/AssemblyDependency/AssemblyFoldersFromConfig/AssemblyFoldersFromConfigResolver.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; @@ -75,10 +75,10 @@ internal class AssemblyFoldersFromConfigResolver : Resolver public AssemblyFoldersFromConfigResolver(string searchPathElement, GetAssemblyName getAssemblyName, FileExists fileExists, GetAssemblyRuntimeVersion getRuntimeVersion, Version targetedRuntimeVesion, ProcessorArchitecture targetProcessorArchitecture, bool compareProcessorArchitecture, - IBuildEngine buildEngine, TaskLoggingHelper log) + IBuildEngine buildEngine, TaskLoggingHelper log, TaskEnvironment taskEnvironment) : base( searchPathElement, getAssemblyName, fileExists, getRuntimeVersion, targetedRuntimeVesion, - targetProcessorArchitecture, compareProcessorArchitecture) + targetProcessorArchitecture, compareProcessorArchitecture, taskEnvironment) { _buildEngine = buildEngine as IBuildEngine4; _taskLogger = log; @@ -115,7 +115,7 @@ private void LazyInitialize() _wasMatch = true; - bool useCache = Environment.GetEnvironmentVariable("MSBUILDDISABLEASSEMBLYFOLDERSEXCACHE") == null; + bool useCache = taskEnvironment.GetEnvironmentVariable("MSBUILDDISABLEASSEMBLYFOLDERSEXCACHE") == null; string key = "6f7de854-47fe-4ae2-9cfe-9b33682abd91" + searchPathElement; if (useCache && _buildEngine != null) @@ -133,7 +133,7 @@ private void LazyInitialize() try { AssemblyFoldersFromConfig assemblyFolders = new AssemblyFoldersFromConfig(_assemblyFolderConfigFile, _targetRuntimeVersion, targetProcessorArchitecture); - _assemblyFoldersCache = new AssemblyFoldersFromConfigCache(assemblyFolders, fileExists); + _assemblyFoldersCache = new AssemblyFoldersFromConfigCache(assemblyFolders, fileExists, taskEnvironment); if (useCache) { _buildEngine?.RegisterTaskObject(key, _assemblyFoldersCache, RegisteredTaskObjectLifetime.Build, true /* dispose early ok*/); @@ -180,7 +180,24 @@ public override bool Resolve( { foreach (AssemblyFoldersFromConfigInfo assemblyFolder in _assemblyFoldersCache.AssemblyFoldersFromConfig) { - string candidatePath = ResolveFromDirectory(assemblyName, isPrimaryProjectReference, wantSpecificVersion, executableExtensions, assemblyFolder.DirectoryPath, assembliesConsideredAndRejected); + string directoryPath; + if (string.IsNullOrEmpty(assemblyFolder.DirectoryPath)) + { + if (ChangeWaves.AreFeaturesEnabled(ChangeWaves.Wave18_8)) + { + continue; + } + + // Pre-Wave18_8: empty DirectoryPath was passed through and resolved from CWD (project directory). + directoryPath = taskEnvironment.ProjectDirectory.Value; + } + else + { + // Absolutize defensively — config paths may not be absolute. + directoryPath = taskEnvironment.GetAbsolutePath(assemblyFolder.DirectoryPath).Value; + } + + string candidatePath = ResolveFromDirectory(assemblyName, isPrimaryProjectReference, wantSpecificVersion, executableExtensions, directoryPath, assembliesConsideredAndRejected); // We have a full path returned if (candidatePath != null) diff --git a/src/Tasks/AssemblyDependency/AssemblyFoldersResolver.cs b/src/Tasks/AssemblyDependency/AssemblyFoldersResolver.cs index 9d1d89e75ab..c90b5c905ac 100644 --- a/src/Tasks/AssemblyDependency/AssemblyFoldersResolver.cs +++ b/src/Tasks/AssemblyDependency/AssemblyFoldersResolver.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using Microsoft.Build.Framework; using Microsoft.Build.Shared; #nullable disable @@ -22,8 +23,9 @@ internal class AssemblyFoldersResolver : Resolver /// Delegate that returns if the file exists. /// Delegate that returns the clr runtime version for the file. /// The targeted runtime version. - public AssemblyFoldersResolver(string searchPathElement, GetAssemblyName getAssemblyName, FileExists fileExists, GetAssemblyRuntimeVersion getRuntimeVersion, Version targetedRuntimeVesion) - : base(searchPathElement, getAssemblyName, fileExists, getRuntimeVersion, targetedRuntimeVesion, System.Reflection.ProcessorArchitecture.None, false) + /// TaskEnvironment for thread-safe environment variable access and path resolution. + public AssemblyFoldersResolver(string searchPathElement, GetAssemblyName getAssemblyName, FileExists fileExists, GetAssemblyRuntimeVersion getRuntimeVersion, Version targetedRuntimeVesion, TaskEnvironment taskEnvironment) + : base(searchPathElement, getAssemblyName, fileExists, getRuntimeVersion, targetedRuntimeVesion, System.Reflection.ProcessorArchitecture.None, false, taskEnvironment) { } @@ -52,7 +54,8 @@ public override bool Resolve( // {AssemblyFolders} was passed in. foreach (string assemblyFolder in AssemblyFolder.GetAssemblyFolders(assemblyFolderKey)) { - string resolvedPath = ResolveFromDirectory(assemblyName, isPrimaryProjectReference, wantSpecificVersion, executableExtensions, assemblyFolder, assembliesConsideredAndRejected); + string fullAssemblyFolder = taskEnvironment.GetAbsolutePath(assemblyFolder).Value; + string resolvedPath = ResolveFromDirectory(assemblyName, isPrimaryProjectReference, wantSpecificVersion, executableExtensions, fullAssemblyFolder, assembliesConsideredAndRejected); if (resolvedPath != null) { foundPath = resolvedPath; diff --git a/src/Tasks/AssemblyDependency/AssemblyResolution.cs b/src/Tasks/AssemblyDependency/AssemblyResolution.cs index a5bf72a3616..ce56a1419de 100644 --- a/src/Tasks/AssemblyDependency/AssemblyResolution.cs +++ b/src/Tasks/AssemblyDependency/AssemblyResolution.cs @@ -120,6 +120,7 @@ internal static string ResolveReference( /// /// /// + /// TaskEnvironment for thread-safe environment variable access. /// #else /// @@ -129,7 +130,7 @@ internal static string ResolveReference( /// /// Paths to assembly files mentioned in the project. /// Like x86 or IA64\AMD64, the processor architecture being targetted. - /// Paths to FX folders. + /// Full paths to FX folders. /// /// /// @@ -137,6 +138,7 @@ internal static string ResolveReference( /// /// /// + /// TaskEnvironment for thread-safe environment variable access. /// #endif public static Resolver[] CompileSearchPaths( @@ -156,7 +158,8 @@ public static Resolver[] CompileSearchPaths( GetAssemblyRuntimeVersion getRuntimeVersion, Version targetedRuntimeVersion, GetAssemblyPathInGac getAssemblyPathInGac, - TaskLoggingHelper log) + TaskLoggingHelper log, + TaskEnvironment taskEnvironment) { var resolvers = new Resolver[searchPaths.Length]; @@ -168,44 +171,44 @@ public static Resolver[] CompileSearchPaths( // HintPath property. if (String.Equals(basePath, AssemblyResolutionConstants.hintPathSentinel, StringComparison.OrdinalIgnoreCase)) { - resolvers[p] = new HintPathResolver(searchPaths[p], getAssemblyName, fileExists, getRuntimeVersion, targetedRuntimeVersion); + resolvers[p] = new HintPathResolver(searchPaths[p], getAssemblyName, fileExists, getRuntimeVersion, targetedRuntimeVersion, taskEnvironment); } else if (String.Equals(basePath, AssemblyResolutionConstants.frameworkPathSentinel, StringComparison.OrdinalIgnoreCase)) { - resolvers[p] = new FrameworkPathResolver(frameworkPaths, installedAssemblies, searchPaths[p], getAssemblyName, fileExists, getRuntimeVersion, targetedRuntimeVersion); + resolvers[p] = new FrameworkPathResolver(frameworkPaths, installedAssemblies, searchPaths[p], getAssemblyName, fileExists, getRuntimeVersion, targetedRuntimeVersion, taskEnvironment); } else if (String.Equals(basePath, AssemblyResolutionConstants.rawFileNameSentinel, StringComparison.OrdinalIgnoreCase)) { - resolvers[p] = new RawFilenameResolver(searchPaths[p], getAssemblyName, fileExists, getRuntimeVersion, targetedRuntimeVersion); + resolvers[p] = new RawFilenameResolver(searchPaths[p], getAssemblyName, fileExists, getRuntimeVersion, targetedRuntimeVersion, taskEnvironment); } else if (String.Equals(basePath, AssemblyResolutionConstants.candidateAssemblyFilesSentinel, StringComparison.OrdinalIgnoreCase)) { - resolvers[p] = new CandidateAssemblyFilesResolver(candidateAssemblyFiles, searchPaths[p], getAssemblyName, fileExists, getRuntimeVersion, targetedRuntimeVersion); + resolvers[p] = new CandidateAssemblyFilesResolver(candidateAssemblyFiles, searchPaths[p], getAssemblyName, fileExists, getRuntimeVersion, targetedRuntimeVersion, taskEnvironment); } #if FEATURE_GAC else if (String.Equals(basePath, AssemblyResolutionConstants.gacSentinel, StringComparison.OrdinalIgnoreCase)) { - resolvers[p] = new GacResolver(targetProcessorArchitecture, searchPaths[p], getAssemblyName, fileExists, getRuntimeVersion, targetedRuntimeVersion, getAssemblyPathInGac); + resolvers[p] = new GacResolver(targetProcessorArchitecture, searchPaths[p], getAssemblyName, fileExists, getRuntimeVersion, targetedRuntimeVersion, getAssemblyPathInGac, taskEnvironment); } #endif else if (String.Equals(basePath, AssemblyResolutionConstants.assemblyFoldersSentinel, StringComparison.OrdinalIgnoreCase)) { - resolvers[p] = new AssemblyFoldersResolver(searchPaths[p], getAssemblyName, fileExists, getRuntimeVersion, targetedRuntimeVersion); + resolvers[p] = new AssemblyFoldersResolver(searchPaths[p], getAssemblyName, fileExists, getRuntimeVersion, targetedRuntimeVersion, taskEnvironment); } #if FEATURE_WIN32_REGISTRY // 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); + resolvers[p] = new AssemblyFoldersExResolver(searchPaths[p], getAssemblyName, fileExists, getRegistrySubKeyNames, getRegistrySubKeyDefaultValue, getRuntimeVersion, openBaseKey, targetedRuntimeVersion, targetProcessorArchitecture, true, buildEngine, taskEnvironment); } #endif else if (0 == String.Compare(basePath, 0, AssemblyResolutionConstants.assemblyFoldersFromConfigSentinel, 0, AssemblyResolutionConstants.assemblyFoldersFromConfigSentinel.Length, StringComparison.OrdinalIgnoreCase)) { - resolvers[p] = new AssemblyFoldersFromConfigResolver(searchPaths[p], getAssemblyName, fileExists, getRuntimeVersion, targetedRuntimeVersion, targetProcessorArchitecture, true, buildEngine, log); + resolvers[p] = new AssemblyFoldersFromConfigResolver(searchPaths[p], getAssemblyName, fileExists, getRuntimeVersion, targetedRuntimeVersion, targetProcessorArchitecture, true, buildEngine, log, taskEnvironment); } else { - resolvers[p] = new DirectoryResolver(searchPaths[p], getAssemblyName, fileExists, getRuntimeVersion, targetedRuntimeVersion, null); + resolvers[p] = new DirectoryResolver(searchPaths[p], getAssemblyName, fileExists, getRuntimeVersion, targetedRuntimeVersion, null, taskEnvironment); } } return resolvers; @@ -219,12 +222,13 @@ internal static Resolver[] CompileDirectories( FileExists fileExists, GetAssemblyName getAssemblyName, GetAssemblyRuntimeVersion getRuntimeVersion, - Version targetedRuntimeVersion) + Version targetedRuntimeVersion, + TaskEnvironment taskEnvironment) { var resolvers = new Resolver[parentReferenceDirectories.Count]; for (int i = 0; i < parentReferenceDirectories.Count; i++) { - resolvers[i] = new DirectoryResolver(parentReferenceDirectories[i].Directory, getAssemblyName, fileExists, getRuntimeVersion, targetedRuntimeVersion, parentReferenceDirectories[i].ParentAssembly); + resolvers[i] = new DirectoryResolver(parentReferenceDirectories[i].Directory, getAssemblyName, fileExists, getRuntimeVersion, targetedRuntimeVersion, parentReferenceDirectories[i].ParentAssembly, taskEnvironment); } return resolvers; diff --git a/src/Tasks/AssemblyDependency/CandidateAssemblyFilesResolver.cs b/src/Tasks/AssemblyDependency/CandidateAssemblyFilesResolver.cs index 99bda929a54..f990741d79c 100644 --- a/src/Tasks/AssemblyDependency/CandidateAssemblyFilesResolver.cs +++ b/src/Tasks/AssemblyDependency/CandidateAssemblyFilesResolver.cs @@ -25,14 +25,15 @@ internal class CandidateAssemblyFilesResolver : Resolver /// /// Construct. /// - /// List of literal assembly file names to be considered when SearchPaths has {CandidateAssemblyFiles}. + /// List of literal assembly file names (full paths) to be considered when SearchPaths has {CandidateAssemblyFiles}. /// The corresponding element from the search path. /// Delegate that gets the assembly name. /// Delegate that returns if the file exists. /// Delegate that returns the clr runtime version for the file. /// The targeted runtime version. - public CandidateAssemblyFilesResolver(string[] candidateAssemblyFiles, string searchPathElement, GetAssemblyName getAssemblyName, FileExists fileExists, GetAssemblyRuntimeVersion getRuntimeVersion, Version targetedRuntimeVesion) - : base(searchPathElement, getAssemblyName, fileExists, getRuntimeVersion, targetedRuntimeVesion, ProcessorArchitecture.None, false) + /// TaskEnvironment for thread-safe environment variable access and path resolution. + public CandidateAssemblyFilesResolver(string[] candidateAssemblyFiles, string searchPathElement, GetAssemblyName getAssemblyName, FileExists fileExists, GetAssemblyRuntimeVersion getRuntimeVersion, Version targetedRuntimeVesion, TaskEnvironment taskEnvironment) + : base(searchPathElement, getAssemblyName, fileExists, getRuntimeVersion, targetedRuntimeVesion, ProcessorArchitecture.None, false, taskEnvironment) { _candidateAssemblyFiles = candidateAssemblyFiles; } diff --git a/src/Tasks/AssemblyDependency/DirectoryResolver.cs b/src/Tasks/AssemblyDependency/DirectoryResolver.cs index f5b7947bf73..28c31af46cf 100644 --- a/src/Tasks/AssemblyDependency/DirectoryResolver.cs +++ b/src/Tasks/AssemblyDependency/DirectoryResolver.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using Microsoft.Build.Framework; using Microsoft.Build.Shared; #nullable disable @@ -19,13 +20,19 @@ internal class DirectoryResolver : Resolver /// public readonly string parentAssembly; + /// + /// Cached absolute path for the search path element. Not necessarily in canonical form. + /// + private readonly string _fullSearchPath; + /// /// Construct. /// - public DirectoryResolver(string searchPathElement, GetAssemblyName getAssemblyName, FileExists fileExists, GetAssemblyRuntimeVersion getRuntimeVersion, Version targetedRuntimeVesion, string parentAssembly) - : base(searchPathElement, getAssemblyName, fileExists, getRuntimeVersion, targetedRuntimeVesion, System.Reflection.ProcessorArchitecture.None, false) + public DirectoryResolver(string searchPathElement, GetAssemblyName getAssemblyName, FileExists fileExists, GetAssemblyRuntimeVersion getRuntimeVersion, Version targetedRuntimeVesion, string parentAssembly, TaskEnvironment taskEnvironment) + : base(searchPathElement, getAssemblyName, fileExists, getRuntimeVersion, targetedRuntimeVesion, System.Reflection.ProcessorArchitecture.None, false, taskEnvironment) { this.parentAssembly = parentAssembly; + _fullSearchPath = string.IsNullOrEmpty(searchPathElement) ? searchPathElement : taskEnvironment.GetAbsolutePath(searchPathElement).Value; } /// @@ -53,7 +60,7 @@ public override bool Resolve( var searchLocationsWithParentAssembly = new List(); // Resolve to the given path. - resolvedPath = ResolveFromDirectory(assemblyName, isPrimaryProjectReference, wantSpecificVersion, executableExtensions, searchPathElement, searchLocationsWithParentAssembly); + resolvedPath = ResolveFromDirectory(assemblyName, isPrimaryProjectReference, wantSpecificVersion, executableExtensions, _fullSearchPath, searchLocationsWithParentAssembly); foreach (var searchLocation in searchLocationsWithParentAssembly) { @@ -64,7 +71,7 @@ public override bool Resolve( } else { - resolvedPath = ResolveFromDirectory(assemblyName, isPrimaryProjectReference, wantSpecificVersion, executableExtensions, searchPathElement, assembliesConsideredAndRejected); + resolvedPath = ResolveFromDirectory(assemblyName, isPrimaryProjectReference, wantSpecificVersion, executableExtensions, _fullSearchPath, assembliesConsideredAndRejected); } if (resolvedPath != null) diff --git a/src/Tasks/AssemblyDependency/FrameworkPathResolver.cs b/src/Tasks/AssemblyDependency/FrameworkPathResolver.cs index cd57aa7d6b5..3d7db166ece 100644 --- a/src/Tasks/AssemblyDependency/FrameworkPathResolver.cs +++ b/src/Tasks/AssemblyDependency/FrameworkPathResolver.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using Microsoft.Build.Framework; using Microsoft.Build.Shared; #nullable disable @@ -23,8 +24,8 @@ internal class FrameworkPathResolver : Resolver /// /// Construct. /// - 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) + public FrameworkPathResolver(string[] frameworkPaths, InstalledAssemblies installedAssemblies, string searchPathElement, GetAssemblyName getAssemblyName, FileExists fileExists, GetAssemblyRuntimeVersion getRuntimeVersion, Version targetedRuntimeVesion, TaskEnvironment taskEnvironment) + : base(searchPathElement, getAssemblyName, fileExists, getRuntimeVersion, targetedRuntimeVesion, System.Reflection.ProcessorArchitecture.None, false, taskEnvironment) { _frameworkPaths = frameworkPaths; _installedAssemblies = installedAssemblies; diff --git a/src/Tasks/AssemblyDependency/GacResolver.cs b/src/Tasks/AssemblyDependency/GacResolver.cs index bf416cc56ed..c48062f7a98 100644 --- a/src/Tasks/AssemblyDependency/GacResolver.cs +++ b/src/Tasks/AssemblyDependency/GacResolver.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using Microsoft.Build.Framework; using Microsoft.Build.Shared; #nullable disable @@ -29,8 +30,9 @@ internal class GacResolver : Resolver /// Delegate to get the runtime version. /// The targeted runtime version. /// Delegate to get assembly path in the GAC. - public GacResolver(System.Reflection.ProcessorArchitecture targetProcessorArchitecture, string searchPathElement, GetAssemblyName getAssemblyName, FileExists fileExists, GetAssemblyRuntimeVersion getRuntimeVersion, Version targetedRuntimeVesion, GetAssemblyPathInGac getAssemblyPathInGac) - : base(searchPathElement, getAssemblyName, fileExists, getRuntimeVersion, targetedRuntimeVesion, targetProcessorArchitecture, true) + /// TaskEnvironment for thread-safe environment variable access and path resolution. + public GacResolver(System.Reflection.ProcessorArchitecture targetProcessorArchitecture, string searchPathElement, GetAssemblyName getAssemblyName, FileExists fileExists, GetAssemblyRuntimeVersion getRuntimeVersion, Version targetedRuntimeVesion, GetAssemblyPathInGac getAssemblyPathInGac, TaskEnvironment taskEnvironment) + : base(searchPathElement, getAssemblyName, fileExists, getRuntimeVersion, targetedRuntimeVesion, targetProcessorArchitecture, true, taskEnvironment) { _getAssemblyPathInGac = getAssemblyPathInGac; } diff --git a/src/Tasks/AssemblyDependency/GlobalAssemblyCache.cs b/src/Tasks/AssemblyDependency/GlobalAssemblyCache.cs index 1a6656c607d..772709798f0 100644 --- a/src/Tasks/AssemblyDependency/GlobalAssemblyCache.cs +++ b/src/Tasks/AssemblyDependency/GlobalAssemblyCache.cs @@ -238,6 +238,7 @@ private static string CheckForFullFusionNameInGac(AssemblyNameExtension assembly /// Delegate to get path to a file based on the fusion name. /// Delegate to get the enumerator which will enumerate over the GAC. /// Whether to check for a specific version. + /// TaskEnvironment for thread-safe operations. /// The path to the assembly. Empty if none exists. internal static string GetLocation( AssemblyNameExtension strongName, @@ -248,9 +249,10 @@ internal static string GetLocation( FileExists fileExists, GetPathFromFusionName getPathFromFusionName, GetGacEnumerator getGacEnumerator, - bool specificVersion) + bool specificVersion, + TaskEnvironment taskEnvironment) { - return GetLocation(null, strongName, targetProcessorArchitecture, getRuntimeVersion, targetedRuntimeVersion, fullFusionName, fileExists, getPathFromFusionName, getGacEnumerator, specificVersion); + return GetLocation(null, strongName, targetProcessorArchitecture, getRuntimeVersion, targetedRuntimeVersion, fullFusionName, fileExists, getPathFromFusionName, getGacEnumerator, specificVersion, taskEnvironment); } /// @@ -266,6 +268,7 @@ internal static string GetLocation( /// Delegate to get path to a file based on the fusion name. /// Delegate to get the enumerator which will enumerate over the GAC. /// Whether to check for a specific version. + /// Optional TaskEnvironment for thread-safe environment variable access. /// The path to the assembly. Empty if none exists. internal static string GetLocation( IBuildEngine4 buildEngine, @@ -277,10 +280,11 @@ internal static string GetLocation( FileExists fileExists, GetPathFromFusionName getPathFromFusionName, GetGacEnumerator getGacEnumerator, - bool specificVersion) + bool specificVersion, + TaskEnvironment taskEnvironment) { ConcurrentDictionary fusionNameToResolvedPath = null; - bool useGacRarCache = Environment.GetEnvironmentVariable("MSBUILDDISABLEGACRARCACHE") == null; + bool useGacRarCache = taskEnvironment.GetEnvironmentVariable("MSBUILDDISABLEGACRARCACHE") == null; if (buildEngine != null && useGacRarCache) { string key = $"44d78b60-3bbe-48fe-9493-04119ebf515f|{targetProcessorArchitecture}|{targetedRuntimeVersion}|{fullFusionName}|{specificVersion}"; diff --git a/src/Tasks/AssemblyDependency/HintPathResolver.cs b/src/Tasks/AssemblyDependency/HintPathResolver.cs index 08c6d97ef52..05ceb6e2dcd 100644 --- a/src/Tasks/AssemblyDependency/HintPathResolver.cs +++ b/src/Tasks/AssemblyDependency/HintPathResolver.cs @@ -19,8 +19,8 @@ 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) + public HintPathResolver(string searchPathElement, GetAssemblyName getAssemblyName, FileExists fileExists, GetAssemblyRuntimeVersion getRuntimeVersion, Version targetedRuntimeVesion, TaskEnvironment taskEnvironment) + : base(searchPathElement, getAssemblyName, fileExists, getRuntimeVersion, targetedRuntimeVesion, ProcessorArchitecture.None, false, taskEnvironment) { } @@ -45,10 +45,11 @@ public override bool Resolve( // However, we should consider Trim() the hintpath https://github.com/dotnet/msbuild/issues/4603 if (!string.IsNullOrEmpty(hintPath) && !FileUtilities.PathIsInvalid(hintPath)) { - if (ResolveAsFile(FileUtilities.NormalizePath(hintPath), assemblyName, isPrimaryProjectReference, wantSpecificVersion, true, assembliesConsideredAndRejected)) + string fullHintPath = taskEnvironment.GetAbsolutePath(hintPath).Value; + if (ResolveAsFile(fullHintPath, assemblyName, isPrimaryProjectReference, wantSpecificVersion, true, assembliesConsideredAndRejected)) { userRequestedSpecificFile = true; - foundPath = hintPath; + foundPath = fullHintPath; return true; } } diff --git a/src/Tasks/AssemblyDependency/Node/OutOfProcRarClient.cs b/src/Tasks/AssemblyDependency/Node/OutOfProcRarClient.cs index 45ce336909e..2780982fcaf 100644 --- a/src/Tasks/AssemblyDependency/Node/OutOfProcRarClient.cs +++ b/src/Tasks/AssemblyDependency/Node/OutOfProcRarClient.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; +using System.Collections.Generic; using Microsoft.Build.BackEnd; using Microsoft.Build.Framework; using Microsoft.Build.Internal; @@ -12,32 +13,56 @@ namespace Microsoft.Build.Tasks.AssemblyDependency /// /// Implements a client for sending the ResolveAssemblyReference task to an out-of-proc node. /// This is intended to be reused for all RAR tasks across a single build. + /// It manages a pool of pipe clients to the RAR node, allowing it to be used concurrently from multiple RAR tasks. /// internal sealed class OutOfProcRarClient : IDisposable { // Create a single cached instance for this build. internal const string TaskObjectCacheKey = "OutOfProcRarClient"; - private readonly NodePipeClient _pipeClient; + private readonly Queue _availablePipeClients = new(); + private readonly LockType _poolLock = new(); + private readonly ServerNodeHandshake _handshake; + private readonly string _pipeName; + private volatile bool _disposed = false; private OutOfProcRarClient() { - ServerNodeHandshake handshake = new(HandshakeOptions.None); - _pipeClient = new NodePipeClient(NamedPipeUtil.GetRarNodeEndpointPipeName(handshake), handshake); - - NodePacketFactory packetFactory = new(); - packetFactory.RegisterPacketHandler(NodePacketType.RarNodeBufferedLogEvents, static t => new RarNodeBufferedLogEvents(t), null); - packetFactory.RegisterPacketHandler(NodePacketType.RarNodeExecuteResponse, static t => new RarNodeExecuteResponse(t), null); - _pipeClient.RegisterPacketFactory(packetFactory); + _handshake = new(HandshakeOptions.None); + _pipeName = NamedPipeUtil.GetRarNodeEndpointPipeName(_handshake); } - public void Dispose() => _pipeClient.Dispose(); + public void Dispose() + { + if (_disposed) + { + return; + } + + lock (_poolLock) + { + if (_disposed) + { + return; + } + + _disposed = true; + + // Dispose all pipe clients in the pool + CommunicationsUtilities.Trace($"Disposing RAR client pool with {_availablePipeClients.Count} pipe clients."); + while (_availablePipeClients.Count > 0) + { + NodePipeClient pipeClient = _availablePipeClients.Dequeue(); + pipeClient?.Dispose(); + } + } + } internal static OutOfProcRarClient GetInstance(IBuildEngine10 buildEngine) { - // We want to reuse the pipe client across all RAR invocations within a build, but release the connection once - // the MSBuild node is idle. Using RegisteredTaskObjectLifetime.Build ensures that the RAR client is disposed between - // builds, freeing the server to run other requests. + // We want to reuse the pipe client pool across all RAR invocations within a build, but release + // all pipe clients once build is complete. Using RegisteredTaskObjectLifetime.Build ensures + // that the RAR client is disposed between builds, freeing all pipe clients. OutOfProcRarClient rarClient = (OutOfProcRarClient)buildEngine.GetRegisteredTaskObject(TaskObjectCacheKey, RegisteredTaskObjectLifetime.Build); if (rarClient == null) @@ -50,59 +75,130 @@ internal static OutOfProcRarClient GetInstance(IBuildEngine10 buildEngine) return rarClient; } - internal bool Execute(ResolveAssemblyReference rarTask) + private NodePipeClient AcquireConnection() + { + lock (_poolLock) + { + if (_disposed) + { + throw new ObjectDisposedException(nameof(OutOfProcRarClient)); + } + + // Try to get an existing pipe client from the pool + if (_availablePipeClients.Count > 0) + { + return _availablePipeClients.Dequeue(); + } + } + + // No available pipe client, create a new one outside the lock + NodePipeClient newPipeClient = CreateNewConnection(); + CommunicationsUtilities.Trace("Created new pipe client for RAR pool."); + return newPipeClient; + } + + private void ReleaseConnection(NodePipeClient pipeClient) { - // This should only be true at the start of a build. - if (!_pipeClient.IsConnected) + lock (_poolLock) { - // Don't set a timeout since the build manager already blocks until the server is running. - if (!_pipeClient.ConnectToServer(0)) + if (_disposed) { - return false; + // If the client is already disposed, dispose the pipe client instead of returning it to the pool. + CommunicationsUtilities.Trace("RAR client already disposed, disposing pipe client instead of returning to pool."); + pipeClient?.Dispose(); + return; } + + _availablePipeClients.Enqueue(pipeClient); } + } + + private NodePipeClient CreateNewConnection() + { + var pipeClient = new NodePipeClient(_pipeName, _handshake); + + NodePacketFactory packetFactory = new(); + packetFactory.RegisterPacketHandler(NodePacketType.RarNodeBufferedLogEvents, static t => new RarNodeBufferedLogEvents(t), null); + packetFactory.RegisterPacketHandler(NodePacketType.RarNodeExecuteResponse, static t => new RarNodeExecuteResponse(t), null); + pipeClient.RegisterPacketFactory(packetFactory); + + return pipeClient; + } + + internal bool Execute(ResolveAssemblyReference rarTask) + { + // Acquire a pipe client from the pool + NodePipeClient? pipeClient = null; + + try + { + // CA2000: Disposal is handled - ReleaseConnection() either returns + // the pipe client to the pool for reuse or disposes it if this client was already disposed. + // At the end of the build the entire pool will be disposed, which will dispose all pipe clients. +#pragma warning disable CA2000 + pipeClient = AcquireConnection(); +#pragma warning restore CA2000 + + // Connect if not already connected + if (!pipeClient.IsConnected) + { + // Don't set a timeout since the build manager already blocks until the server is running. + if (!pipeClient.ConnectToServer(0)) + { + return false; + } + } - _pipeClient.WritePacket(new RarNodeExecuteRequest(rarTask)); + pipeClient.WritePacket(new RarNodeExecuteRequest(rarTask)); - INodePacket packet = _pipeClient.ReadPacket(); + INodePacket packet = pipeClient.ReadPacket(); - while (packet.Type != NodePacketType.RarNodeExecuteResponse) - { - if (packet.Type == NodePacketType.RarNodeBufferedLogEvents) + while (packet.Type != NodePacketType.RarNodeExecuteResponse) { - RarNodeBufferedLogEvents logEvents = (RarNodeBufferedLogEvents)packet; - foreach (LogMessagePacketBase logMessagePacket in logEvents.EventQueue) + if (packet.Type == NodePacketType.RarNodeBufferedLogEvents) { - BuildEventArgs buildEvent = logMessagePacket.NodeBuildEvent?.Value!; - switch (logMessagePacket.EventType) + RarNodeBufferedLogEvents logEvents = (RarNodeBufferedLogEvents)packet; + foreach (LogMessagePacketBase logMessagePacket in logEvents.EventQueue) { - case LoggingEventType.BuildErrorEvent: - rarTask.BuildEngine.LogErrorEvent((BuildErrorEventArgs)buildEvent); - break; - case LoggingEventType.BuildWarningEvent: - rarTask.BuildEngine.LogWarningEvent((BuildWarningEventArgs)buildEvent); - break; - case LoggingEventType.BuildMessageEvent: - rarTask.BuildEngine.LogMessageEvent((BuildMessageEventArgs)buildEvent); - break; - default: - ErrorUtilities.ThrowInternalError($"Received unexpected log event type {logMessagePacket.Type}"); - break; + BuildEventArgs buildEvent = logMessagePacket.NodeBuildEvent?.Value!; + switch (logMessagePacket.EventType) + { + case LoggingEventType.BuildErrorEvent: + rarTask.BuildEngine.LogErrorEvent((BuildErrorEventArgs)buildEvent); + break; + case LoggingEventType.BuildWarningEvent: + rarTask.BuildEngine.LogWarningEvent((BuildWarningEventArgs)buildEvent); + break; + case LoggingEventType.BuildMessageEvent: + rarTask.BuildEngine.LogMessageEvent((BuildMessageEventArgs)buildEvent); + break; + default: + ErrorUtilities.ThrowInternalError($"Received unexpected log event type {logMessagePacket.Type}"); + break; + } } } + else + { + ErrorUtilities.ThrowInternalError($"Received unexpected packet type {packet.Type}"); + } + + packet = pipeClient.ReadPacket(); } - else + + RarNodeExecuteResponse response = (RarNodeExecuteResponse)packet; + response.SetTaskOutputs(rarTask); + + return response.Success; + } + finally + { + // Return the pipe client to the pool + if (pipeClient != null) { - ErrorUtilities.ThrowInternalError($"Received unexpected packet type {packet.Type}"); + ReleaseConnection(pipeClient); } - - packet = _pipeClient.ReadPacket(); } - - RarNodeExecuteResponse response = (RarNodeExecuteResponse)packet; - response.SetTaskOutputs(rarTask); - - return response.Success; } } } diff --git a/src/Tasks/AssemblyDependency/Node/OutOfProcRarNodeEndpoint.cs b/src/Tasks/AssemblyDependency/Node/OutOfProcRarNodeEndpoint.cs index e6576665003..65da350e1cd 100644 --- a/src/Tasks/AssemblyDependency/Node/OutOfProcRarNodeEndpoint.cs +++ b/src/Tasks/AssemblyDependency/Node/OutOfProcRarNodeEndpoint.cs @@ -5,6 +5,7 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.Build.BackEnd; +using Microsoft.Build.Framework; using Microsoft.Build.Internal; using Microsoft.Build.Shared; @@ -96,15 +97,22 @@ private async Task RunInternalAsync(CancellationToken cancellationToken) RarNodeExecuteRequest request = (RarNodeExecuteRequest)packet; ResolveAssemblyReference rarTask = new(); - request.SetTaskInputs(rarTask, _buildEngine); - - bool success = rarTask.Execute(); - - // Send any remaining log events before returning the final result packet. - await _buildEngine.FlushEventsAsync(cancellationToken).ConfigureAwait(false); - await _pipeServer.WritePacketAsync(new RarNodeExecuteResponse(rarTask, success), cancellationToken).ConfigureAwait(false); - - CommunicationsUtilities.Trace($"({_endpointId}) Completed RAR request."); + + // TODO: This needs to be updated when configuration support is added to pass client-specific state + // For now, using the environment variables from the RAR node process. + using (var environmentDriver = new MultiThreadedTaskEnvironmentDriver(request.ProjectDirectory)) + { + rarTask.TaskEnvironment = new TaskEnvironment(environmentDriver); + request.SetTaskInputs(rarTask, _buildEngine); + + bool success = rarTask.Execute(); + + // Send any remaining log events before returning the final result packet. + await _buildEngine.FlushEventsAsync(cancellationToken).ConfigureAwait(false); + await _pipeServer.WritePacketAsync(new RarNodeExecuteResponse(rarTask, success), cancellationToken).ConfigureAwait(false); + + CommunicationsUtilities.Trace($"({_endpointId}) Completed RAR request."); + } break; case NodePacketType.NodeShutdown: // Although the client has already disconnected, it is still necessary to Disconnect() so the diff --git a/src/Tasks/AssemblyDependency/Node/RarNodeExecuteRequest.cs b/src/Tasks/AssemblyDependency/Node/RarNodeExecuteRequest.cs index f5527c509d4..8a0cb4706b7 100644 --- a/src/Tasks/AssemblyDependency/Node/RarNodeExecuteRequest.cs +++ b/src/Tasks/AssemblyDependency/Node/RarNodeExecuteRequest.cs @@ -3,7 +3,6 @@ using System; using System.Collections.Generic; -using System.IO; using Microsoft.Build.BackEnd; using Microsoft.Build.Framework; using ParameterType = Microsoft.Build.Tasks.AssemblyDependency.RarTaskParameters.ParameterType; @@ -19,24 +18,18 @@ internal sealed class RarNodeExecuteRequest : INodePacket private int _lineNumberOfTaskNode; private int _columnNumberOfTaskNode; private string? _projectFileOfTaskNode; + private string _projectDirectory = null!; private MessageImportance _minimumMessageImportance; private bool _isTaskInputLoggingEnabled; internal RarNodeExecuteRequest(ResolveAssemblyReference rar) { - // The RAR node may have a different working directory than the target, so convert potential relative paths to absolute. - if (rar.AppConfigFile != null) - { - rar.AppConfigFile = Path.GetFullPath(rar.AppConfigFile); - } - - if (rar.StateFile != null) - { - rar.StateFile = Path.GetFullPath(rar.StateFile); - } _taskInputs = RarTaskParameters.Get(ParameterType.Input, rar); + // Capture the project directory from TaskEnvironment + _projectDirectory = rar.TaskEnvironment.ProjectDirectory.Value; + // Ensure log messages are identical to those that would be produced on the client. _lineNumberOfTaskNode = rar.BuildEngine.LineNumberOfTaskNode; _columnNumberOfTaskNode = rar.BuildEngine.ColumnNumberOfTaskNode; @@ -49,6 +42,8 @@ internal RarNodeExecuteRequest(ResolveAssemblyReference rar) internal RarNodeExecuteRequest(ITranslator translator) => Translate(translator); + public string ProjectDirectory => _projectDirectory; + public NodePacketType Type => NodePacketType.RarNodeExecuteRequest; public void Translate(ITranslator translator) @@ -60,6 +55,7 @@ public void Translate(ITranslator translator) translator.Translate(ref _lineNumberOfTaskNode); translator.Translate(ref _columnNumberOfTaskNode); translator.Translate(ref _projectFileOfTaskNode); + translator.Translate(ref _projectDirectory); translator.TranslateEnum(ref _minimumMessageImportance, (int)_minimumMessageImportance); translator.Translate(ref _isTaskInputLoggingEnabled); } diff --git a/src/Tasks/AssemblyDependency/Node/RarTaskParameters.cs b/src/Tasks/AssemblyDependency/Node/RarTaskParameters.cs index 54cf77a6430..b3f8b94e14c 100644 --- a/src/Tasks/AssemblyDependency/Node/RarTaskParameters.cs +++ b/src/Tasks/AssemblyDependency/Node/RarTaskParameters.cs @@ -162,7 +162,12 @@ internal ReflectedProperties() } else { - inputs.Add(reflectedProperty); + // Exclude TaskEnvironment since it is not a serializable parameter type for cross-process communication. + // It should be set to each task instance after deserialization. + if (!string.Equals(property.Name, nameof(ResolveAssemblyReference.TaskEnvironment), StringComparison.Ordinal)) + { + inputs.Add(reflectedProperty); + } } } diff --git a/src/Tasks/AssemblyDependency/RawFilenameResolver.cs b/src/Tasks/AssemblyDependency/RawFilenameResolver.cs index 72f1afb79a2..1a18e5d8ee6 100644 --- a/src/Tasks/AssemblyDependency/RawFilenameResolver.cs +++ b/src/Tasks/AssemblyDependency/RawFilenameResolver.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Reflection; +using Microsoft.Build.Framework; using Microsoft.Build.Shared; #nullable disable @@ -18,8 +19,8 @@ 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) + public RawFilenameResolver(string searchPathElement, GetAssemblyName getAssemblyName, FileExists fileExists, GetAssemblyRuntimeVersion getRuntimeVersion, Version targetedRuntimeVesion, TaskEnvironment taskEnvironment) + : base(searchPathElement, getAssemblyName, fileExists, getRuntimeVersion, targetedRuntimeVesion, ProcessorArchitecture.None, false, taskEnvironment) { } @@ -44,10 +45,11 @@ public override bool Resolve( if (rawFileNameCandidate != null) { // {RawFileName} was passed in. - if (isImmutableFrameworkReference || fileExists(rawFileNameCandidate)) + string fullRawFileName = taskEnvironment.GetAbsolutePath(rawFileNameCandidate).Value; + if (isImmutableFrameworkReference || fileExists(fullRawFileName)) { userRequestedSpecificFile = true; - foundPath = rawFileNameCandidate; + foundPath = fullRawFileName; return true; } @@ -55,7 +57,7 @@ public override bool Resolve( { var considered = new ResolutionSearchLocation { - FileNameAttempted = rawFileNameCandidate, + FileNameAttempted = fullRawFileName, SearchPath = searchPathElement, Reason = NoMatchReason.NotAFileNameOnDisk }; diff --git a/src/Tasks/AssemblyDependency/Reference.cs b/src/Tasks/AssemblyDependency/Reference.cs index 8322b4e1897..8eef0415c57 100644 --- a/src/Tasks/AssemblyDependency/Reference.cs +++ b/src/Tasks/AssemblyDependency/Reference.cs @@ -821,6 +821,7 @@ internal void AddAssembliesConsideredAndRejected(List /// /// 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. + /// Note that these paths are not canonicalized — resolvers only absolutize paths, not canonicalize them. /// internal List AssembliesConsideredAndRejected { get; private set; } = new List(); @@ -922,13 +923,7 @@ internal static bool IsFrameworkFile(string fullPath, string[] frameworkPaths) { foreach (string frameworkPath in frameworkPaths) { - if - ( - String.Compare( - frameworkPath, 0, - fullPath, 0, - frameworkPath.Length, - StringComparison.OrdinalIgnoreCase) == 0) + if (IsUnderDirectory(fullPath, frameworkPath)) { return true; } @@ -937,6 +932,36 @@ internal static bool IsFrameworkFile(string fullPath, string[] frameworkPaths) return false; } + /// + /// 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, AbsolutePath[] frameworkPaths) + { + if (frameworkPaths != null) + { + foreach (var frameworkPath in frameworkPaths) + { + if (IsUnderDirectory(fullPath, frameworkPath.Value)) + { + return true; + } + } + } + return false; + } + + /// + /// Checks whether starts with (case-insensitive). + /// + private static bool IsUnderDirectory(string fullPath, string directoryPath) + { + return directoryPath is not null && + String.Compare(directoryPath, 0, fullPath, 0, directoryPath.Length, StringComparison.OrdinalIgnoreCase) == 0; + } + /// /// Figure out the what the CopyLocal state of given assembly should be. /// diff --git a/src/Tasks/AssemblyDependency/ReferenceTable.cs b/src/Tasks/AssemblyDependency/ReferenceTable.cs index 37de75c0dec..ad0fde4c423 100644 --- a/src/Tasks/AssemblyDependency/ReferenceTable.cs +++ b/src/Tasks/AssemblyDependency/ReferenceTable.cs @@ -111,6 +111,11 @@ internal sealed class ReferenceTable /// private readonly ReadMachineTypeFromPEHeader _readMachineTypeFromPEHeader; + /// + /// TaskEnvironment for thread-safe access to environment variables and path resolution. + /// + private readonly TaskEnvironment _taskEnvironment; + /// /// Is the file a winMD file /// @@ -225,6 +230,7 @@ internal sealed class ReferenceTable /// /// /// + /// TaskEnvironment for thread-safe environment variable access and path resolution. #else /// /// Construct. @@ -239,7 +245,7 @@ internal sealed class ReferenceTable /// /// List of literal assembly file names to be considered when SearchPaths has {CandidateAssemblyFiles}. /// Resolved sdk items - /// Path to the FX. + /// Full paths to the FX. /// Installed assembly XML tables. /// Like x86 or IA64\AMD64, the processor architecture being targeted. /// Delegate used for checking for the existence of a file. @@ -265,6 +271,7 @@ internal sealed class ReferenceTable /// /// /// + /// TaskEnvironment for thread-safe environment variable access and path resolution. #endif internal ReferenceTable( IBuildEngine buildEngine, @@ -307,7 +314,8 @@ internal ReferenceTable( bool ignoreFrameworkAttributeVersionMismatch, bool unresolveFrameworkAssembliesFromHigherFrameworks, ConcurrentDictionary assemblyMetadataCache, - string[] nonCultureResourceDirectories) + string[] nonCultureResourceDirectories, + TaskEnvironment taskEnvironment) { _log = log; _findDependencies = findDependencies; @@ -339,6 +347,7 @@ internal ReferenceTable( _assemblyMetadataCache = assemblyMetadataCache; _nonCultureResourceDirectories = nonCultureResourceDirectories; _enableCustomCulture = enableCustomCulture; + _taskEnvironment = taskEnvironment; // Set condition for when to check assembly version against the target framework version _checkAssemblyVersionAgainstTargetFrameworkVersion = unresolveFrameworkAssembliesFromHigherFrameworks || ((_projectTargetFramework ?? ReferenceTable.s_targetFrameworkVersion_40) <= ReferenceTable.s_targetFrameworkVersion_40); @@ -378,7 +387,8 @@ internal ReferenceTable( getRuntimeVersion, targetedRuntimeVersion, getAssemblyPathInGac, - log); + log, + taskEnvironment); } /// @@ -468,7 +478,7 @@ private AssemblyNameExtension NameAssemblyFileReference( if (!Path.IsPathRooted(assemblyFileName)) { - reference.FullPath = Path.GetFullPath(assemblyFileName); + reference.FullPath = _taskEnvironment.GetAbsolutePath(assemblyFileName).GetCanonicalForm(); } else { @@ -477,15 +487,15 @@ private AssemblyNameExtension NameAssemblyFileReference( try { - if (_fileExists(assemblyFileName)) + if (_fileExists(reference.FullPath)) { - assemblyName = _getAssemblyName(assemblyFileName); + assemblyName = _getAssemblyName(reference.FullPath); if (assemblyName != null) { reference.ResolvedSearchPath = assemblyFileName; } } - else if (_directoryExists(assemblyFileName)) + else if (_directoryExists(reference.FullPath)) { assemblyName = new AssemblyNameExtension("*directory*"); @@ -1313,14 +1323,14 @@ private void ResolveReference( // 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 InstalledSDKResolver(_resolvedSDKReferences, "SDKResolver", _getAssemblyName, _fileExists, _getRuntimeVersion, _targetedRuntimeVersion)]); + jaggedResolvers.Add([new InstalledSDKResolver(_resolvedSDKReferences, "SDKResolver", _getAssemblyName, _fileExists, _getRuntimeVersion, _targetedRuntimeVersion, _taskEnvironment)]); } else { // Do not probe near dependees if the reference is primary and resolved externally. If resolved externally, the search paths should have been specified in such a way to point to the assembly file. if (parentReferenceFolders.Count > 0 && (assemblyName == null || !_externallyResolvedPrimaryReferences.Contains(assemblyName.Name))) { - jaggedResolvers.Add(AssemblyResolution.CompileDirectories(parentReferenceFolders, _fileExists, _getAssemblyName, _getRuntimeVersion, _targetedRuntimeVersion)); + jaggedResolvers.Add(AssemblyResolution.CompileDirectories(parentReferenceFolders, _fileExists, _getAssemblyName, _getRuntimeVersion, _targetedRuntimeVersion, _taskEnvironment)); } jaggedResolvers.Add(Resolvers); @@ -1355,7 +1365,14 @@ private void ResolveReference( // If the path was resolved, then specify the full path on the reference. if (resolvedPath != null) { - resolvedPath = FileUtilities.NormalizePath(resolvedPath); + if (ChangeWaves.AreFeaturesEnabled(ChangeWaves.Wave18_8)) + { + resolvedPath = FileUtilities.FixFilePath(_taskEnvironment.GetAbsolutePath(resolvedPath).GetCanonicalForm()).Value; + } + else + { + resolvedPath = FileUtilities.NormalizePath(_taskEnvironment.GetAbsolutePath(resolvedPath)); + } if (isImmutableFrameworkReference) { _externallyResolvedImmutableFiles[resolvedPath] = GetAssemblyNameFromItemMetadata(reference.PrimarySourceItem); diff --git a/src/Tasks/AssemblyDependency/ResolveAssemblyReference.cs b/src/Tasks/AssemblyDependency/ResolveAssemblyReference.cs index cb811cfbfca..a75fb8b2140 100644 --- a/src/Tasks/AssemblyDependency/ResolveAssemblyReference.cs +++ b/src/Tasks/AssemblyDependency/ResolveAssemblyReference.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; @@ -33,7 +33,8 @@ 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, IIncrementalTask + [MSBuildMultiThreadableTask] + public class ResolveAssemblyReference : TaskExtension, IIncrementalTask, IMultiThreadableTask { /// /// key assembly used to trigger inclusion of facade references. @@ -179,13 +180,14 @@ internal static void Initialize(TaskLoggingHelper log) private bool _ignoreDefaultInstalledAssemblyTables = false; private bool _ignoreDefaultInstalledAssemblySubsetTables = false; private bool _enableCustomCulture = false; - private string[] _candidateAssemblyFiles = []; - private string[] _targetFrameworkDirectories = []; + private AbsolutePath[] _candidateAssemblyFiles = []; + private AbsolutePath[] _targetFrameworkDirectories = []; private string[] _nonCultureResourceDirectories = []; private string[] _searchPaths = []; private string[] _allowedAssemblyExtensions = [".winmd", ".dll", ".exe"]; private string[] _relatedFileExtensions = [".pdb", ".xml", ".pri"]; - private string _appConfigFile = null; + private AbsolutePath _appConfigFile = default; + private bool _appConfigValueIsEmptyString = false; private bool _supportsBindingRedirectGeneration; private bool _autoUnify = false; private bool _ignoreVersionForFrameworkReferences = false; @@ -212,12 +214,13 @@ internal static void Initialize(TaskLoggingHelper log) private string _targetedRuntimeVersionRawValue = String.Empty; private Version _projectTargetFramework; - private string _stateFile = null; + private AbsolutePath _stateFile = default; + private AbsolutePath _assemblyInformationCacheOutputPath = default; private string _targetProcessorArchitecture = null; private string _profileName = String.Empty; - private string[] _fullFrameworkFolders = []; - private string[] _latestTargetFrameworkDirectories = []; + private AbsolutePath[] _fullFrameworkFolders = []; + private AbsolutePath[] _latestTargetFrameworkDirectories = []; private bool _copyLocalDependenciesWhenParentReferenceInGac = true; private Dictionary _showAssemblyFoldersExLocations = new Dictionary(StringComparer.OrdinalIgnoreCase); private bool _logVerboseSearchResults = false; @@ -292,12 +295,12 @@ public string[] LatestTargetFrameworkDirectories { get { - return _latestTargetFrameworkDirectories; + return _latestTargetFrameworkDirectories?.Select(path => path.OriginalValue).ToArray(); } set { - _latestTargetFrameworkDirectories = value; + _latestTargetFrameworkDirectories = MakeCanonicalPaths(value); } } @@ -392,15 +395,21 @@ public ITaskItem[] Assemblies /// /// A list of assembly files that can be part of the search and resolution process. - /// These must be absolute filenames, or project-relative filenames. + /// These must be absolute file paths, or project-relative file paths. /// /// 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; } + get + { + return _candidateAssemblyFiles.Select(path => path.OriginalValue).ToArray(); + } + set + { + _candidateAssemblyFiles = MakeAbsolutePaths(value); + } } /// @@ -421,8 +430,14 @@ public ITaskItem[] ResolvedSDKReferences /// public string[] TargetFrameworkDirectories { - get { return _targetFrameworkDirectories; } - set { _targetFrameworkDirectories = value; } + get + { + return _targetFrameworkDirectories?.Select(path => path.OriginalValue).ToArray(); + } + set + { + _targetFrameworkDirectories = MakeCanonicalPaths(value); + } } /// @@ -594,7 +609,14 @@ public string TargetedRuntimeVersion /// If not null, serializes information about inputs to the named file. /// This overrides the usual outputs, so do not use this unless you are building an SDK with many references. /// - public string AssemblyInformationCacheOutputPath { get; set; } + public string AssemblyInformationCacheOutputPath + { + get => _assemblyInformationCacheOutputPath.OriginalValue; + set + { + _assemblyInformationCacheOutputPath = MakeAbsolutePath(value); + } + } /// /// If not null, uses this set of caches as inputs if RAR cannot find the usual cache in the obj folder. Typically @@ -676,8 +698,18 @@ public string[] AllowedRelatedFileExtensions /// public string AppConfigFile { - get { return _appConfigFile; } - set { _appConfigFile = value; } + get => _appConfigFile.OriginalValue; + set + { + if (!string.IsNullOrEmpty(value)) + { + _appConfigFile = MakeAbsolutePath(value); + } + else if (value == string.Empty && !ChangeWaves.AreFeaturesEnabled(ChangeWaves.Wave18_8)) + { + _appConfigValueIsEmptyString = true; + } + } } /// @@ -764,8 +796,11 @@ public bool DoNotCopyLocalIfInGac /// public string StateFile { - get { return _stateFile; } - set { _stateFile = value; } + get => _stateFile.OriginalValue; + set + { + _stateFile = MakeAbsolutePath(value); + } } /// @@ -909,18 +944,21 @@ public string[] FullFrameworkFolders { get { - return _fullFrameworkFolders; + return _fullFrameworkFolders?.Select(path => path.OriginalValue).ToArray(); } set { ErrorUtilities.VerifyThrowArgumentNull(value, "FullFrameworkFolders"); - _fullFrameworkFolders = value; + _fullFrameworkFolders = MakeCanonicalPaths(value); } } public bool FailIfNotIncremental { get; set; } + /// + public TaskEnvironment TaskEnvironment { get; set; } = new TaskEnvironment(MultiProcessTaskEnvironmentDriver.Instance); + /// /// Allow the task to run on the out-of-proc node if enabled for this build. /// @@ -1088,6 +1126,59 @@ public ITaskItem[] UnresolvedAssemblyConflicts get => [.. _unresolvedConflicts]; internal set => _unresolvedConflicts = [.. value]; } + + /// + /// Converts a path to an . Returns default for null or empty paths. + /// Under Wave 18.8, absolutizes relative paths via . + /// Otherwise, wraps the raw string to preserve pre-wave behavior. + /// + private AbsolutePath MakeAbsolutePath(string path) + { + if (string.IsNullOrEmpty(path)) + { + return default; + } + + return ChangeWaves.AreFeaturesEnabled(ChangeWaves.Wave18_8) + ? TaskEnvironment.GetAbsolutePath(path) + : new AbsolutePath(path, ignoreRootedCheck: true); + } + + /// + /// Converts each non-empty path in the array to an . + /// Returns an empty array if is null. + /// + private AbsolutePath[] MakeAbsolutePaths(string[] paths) + { + return paths?.Select(path => MakeAbsolutePath(path)).ToArray() ?? []; + } + + /// + /// Converts a path to an in canonical form (resolves ".." etc.). + /// Returns default for null or empty paths. + /// Canonical form is needed for paths used in string comparisons. + /// Under Wave 18.8, absolutizes and canonicalizes. Otherwise, wraps the raw string. + /// + private AbsolutePath MakeCanonicalPath(string path) + { + if (string.IsNullOrEmpty(path)) + { + return default; + } + + return ChangeWaves.AreFeaturesEnabled(ChangeWaves.Wave18_8) + ? TaskEnvironment.GetAbsolutePath(path).TryGetCanonicalForm(Log) + : new AbsolutePath(path, ignoreRootedCheck: true); + } + + /// + /// Converts each non-empty path in the array to a canonical . + /// Returns an empty array if is null. + /// + private AbsolutePath[] MakeCanonicalPaths(string[] paths) + { + return paths?.Select(path => MakeCanonicalPath(path)).ToArray() ?? []; + } #endregion #region Logging @@ -1533,11 +1624,11 @@ private void LogInputs() } Log.LogMessage(importance, property, "CandidateAssemblyFiles"); - foreach (string file in CandidateAssemblyFiles) + foreach (AbsolutePath file in _candidateAssemblyFiles) { try { - if (FileUtilities.HasExtension(file, _allowedAssemblyExtensions)) + if (FileUtilities.HasExtension(file.OriginalValue, _allowedAssemblyExtensions)) { Log.LogMessage(importance, indent + file); } @@ -1583,7 +1674,7 @@ private void LogInputs() } Log.LogMessage(importance, property, "AppConfigFile"); - Log.LogMessage(importance, $"{indent}{AppConfigFile}"); + Log.LogMessage(importance, $"{indent}{_appConfigFile.OriginalValue}"); Log.LogMessage(importance, property, "AutoUnify"); Log.LogMessage(importance, $"{indent}{AutoUnify}"); @@ -1632,15 +1723,15 @@ private void LogInputs() Log.LogMessage(importance, $"{indent}{ProfileName}"); Log.LogMessage(importance, property, "FullFrameworkFolders"); - foreach (string fullFolder in FullFrameworkFolders) + foreach (var fullFolder in _fullFrameworkFolders) { - Log.LogMessage(importance, $"{indent}{fullFolder}"); + Log.LogMessage(importance, $"{indent}{fullFolder.OriginalValue}"); } Log.LogMessage(importance, property, "LatestTargetFrameworkDirectories"); - foreach (string latestFolder in _latestTargetFrameworkDirectories) + foreach (var latestFolder in _latestTargetFrameworkDirectories) { - Log.LogMessage(importance, $"{indent}{latestFolder}"); + Log.LogMessage(importance, $"{indent}{latestFolder.OriginalValue}"); } Log.LogMessage(importance, property, "ProfileTablesLocation"); @@ -1707,7 +1798,7 @@ private void LogPrimaryOrDependency(Reference reference, string fusionName, Mess } else { - Log.LogMessage(importance, Strings.UnificationByAppConfig, unificationVersion.version, _appConfigFile, unificationVersion.referenceFullPath); + Log.LogMessage(importance, Strings.UnificationByAppConfig, unificationVersion.version, _appConfigFile.OriginalValue, unificationVersion.referenceFullPath); } break; @@ -2107,7 +2198,7 @@ internal void ReadStateFile(FileExists fileExists) // Construct the cache only if we can't find any caches. if (_cache == null && AssemblyInformationCachePaths != null && AssemblyInformationCachePaths.Length > 0) { - _cache = SystemState.DeserializePrecomputedCaches(AssemblyInformationCachePaths, Log, fileExists); + _cache = SystemState.DeserializePrecomputedCaches(AssemblyInformationCachePaths, Log, fileExists, TaskEnvironment); } if (_cache == null) @@ -2121,11 +2212,11 @@ internal void ReadStateFile(FileExists fileExists) /// internal void WriteStateFile() { - if (!string.IsNullOrEmpty(AssemblyInformationCacheOutputPath)) + if (_assemblyInformationCacheOutputPath.Value is not null) { - _cache.SerializePrecomputedCache(AssemblyInformationCacheOutputPath, Log); + _cache.SerializePrecomputedCache(_assemblyInformationCacheOutputPath, Log); } - else if (!string.IsNullOrEmpty(_stateFile) && (_cache.IsDirty || _cache.instanceLocalOutgoingFileStateCache.Count < _cache.instanceLocalFileStateCache.Count)) + else if (_stateFile.Value is not null && (_cache.IsDirty || _cache.instanceLocalOutgoingFileStateCache.Count < _cache.instanceLocalFileStateCache.Count)) { // Either the cache is dirty (we added or updated an item) or the number of items actually used is less than what // we got by reading the state file prior to execution. Serialize the cache into the state file. @@ -2140,10 +2231,10 @@ internal void WriteStateFile() /// private List GetAssemblyRemappingsFromAppConfig() { - if (_appConfigFile != null) + if (_appConfigFile.Value is not null) { AppConfig appConfig = new AppConfig(); - appConfig.Load(_appConfigFile); + appConfig.Load(_appConfigFile.Value); return appConfig.Runtime.DependentAssemblies; } @@ -2237,7 +2328,7 @@ internal bool Execute( return false; } - _logVerboseSearchResults = Environment.GetEnvironmentVariable("MSBUILDLOGVERBOSERARSEARCHRESULTS") != null; + _logVerboseSearchResults = TaskEnvironment.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 @@ -2268,7 +2359,7 @@ internal bool Execute( string subsetOrProfileName = null; // Are we targeting a profile - bool targetingProfile = !String.IsNullOrEmpty(ProfileName) && ((FullFrameworkFolders.Length > 0) || (FullFrameworkAssemblyTables.Length > 0)); + bool targetingProfile = !String.IsNullOrEmpty(ProfileName) && ((_fullFrameworkFolders.Length > 0) || (FullFrameworkAssemblyTables.Length > 0)); bool targetingSubset = false; List inclusionListErrors = new List(); List inclusionListErrorFilesNames = new List(); @@ -2411,10 +2502,25 @@ internal bool Execute( try { appConfigRemappedAssemblies = GetAssemblyRemappingsFromAppConfig(); + + if (!ChangeWaves.AreFeaturesEnabled(ChangeWaves.Wave18_8) && _appConfigValueIsEmptyString) + { + // Preserve backward compatibility for empty AppConfigFile handling. + // Prior to Wave18_8, empty strings would cause TaskEnvironment.GetAbsolutePath() to throw an exception, + // which would be caught and logged as an error, stopping RAR execution. + // With the new behavior, empty strings are silently ignored (treated like null). + // When Wave 18.8 is disabled, we preserve the old failure behavior. + // When cleaning up this change wave, also clean up the _appConfigValueIsEmptyString field. + + // Note, second part of the sentence is not localized: this is a temporary fallback behind a change wave that is off by default + // and will be removed when the wave is cleaned up. Not worth adding a new resource string. + Log.LogErrorWithCodeFromResources("ResolveAssemblyReference.InvalidAppConfig", string.Empty, "AppConfig file path cannot be empty."); + return false; + } } catch (AppConfigException e) { - Log.LogErrorWithCodeFromResources(null, e.FileName, e.Line, e.Column, 0, 0, "ResolveAssemblyReference.InvalidAppConfig", AppConfigFile, e.Message); + Log.LogErrorWithCodeFromResources(null, e.FileName, e.Line, e.Column, 0, 0, "ResolveAssemblyReference.InvalidAppConfig", _appConfigFile.OriginalValue, e.Message); return false; } } @@ -2437,9 +2543,9 @@ internal bool Execute( _searchPaths, _allowedAssemblyExtensions, _relatedFileExtensions, - _candidateAssemblyFiles, + _candidateAssemblyFiles?.Select(path => path.Value).ToArray(), _resolvedSDKReferences, - _targetFrameworkDirectories, + _targetFrameworkDirectories?.Select(path => path.Value).ToArray(), installedAssemblies, processorArchitecture, fileExists, @@ -2457,7 +2563,7 @@ internal bool Execute( _projectTargetFramework, frameworkMoniker, Log, - _latestTargetFrameworkDirectories, + _latestTargetFrameworkDirectories?.Select(path => path.Value).ToArray(), _copyLocalDependenciesWhenParentReferenceInGac, DoNotCopyLocalIfInGac, getAssemblyPathInGac, @@ -2468,7 +2574,8 @@ internal bool Execute( _ignoreTargetFrameworkAttributeVersionMismatch, _unresolveFrameworkAssembliesFromHigherFrameworks, assemblyMetadataCache, - _nonCultureResourceDirectories); + _nonCultureResourceDirectories, + TaskEnvironment); dependencyTable.FindDependenciesOfExternallyResolvedReferences = FindDependenciesOfExternallyResolvedReferences; @@ -2634,9 +2741,9 @@ internal bool Execute( WriteStateFile(); // Save the new state out and put into the file exists if it is actually on disk. - if (_stateFile != null && fileExists(_stateFile)) + if (_stateFile.Value is not null && fileExists(_stateFile.Value)) { - _filesWritten.Add(new TaskItem(_stateFile)); + _filesWritten.Add(new TaskItem(_stateFile.OriginalValue)); } // Log the results. @@ -2822,7 +2929,7 @@ private void HandleProfile(AssemblyTableInfo[] installedAssemblyTableInfo, out A } } - fullRedistAssemblyTableInfo = GetInstalledAssemblyTableInfo(false, FullFrameworkAssemblyTables, new GetListPath(RedistList.GetRedistListPathsFromDisk), FullFrameworkFolders); + fullRedistAssemblyTableInfo = GetInstalledAssemblyTableInfo(false, FullFrameworkAssemblyTables, new GetListPath(RedistList.GetRedistListPathsFromDisk), _fullFrameworkFolders?.Select(path => path.Value).ToArray()); if (fullRedistAssemblyTableInfo.Length > 0) { // Get the redist list which represents the Full framework, we need this so that we can generate the exclusion list @@ -2911,7 +3018,7 @@ private bool VerifyInputConditions() // Make sure the inputs for profiles are correct bool profileNameIsSet = !String.IsNullOrEmpty(ProfileName); - bool fullFrameworkFoldersIsSet = FullFrameworkFolders.Length > 0; + bool fullFrameworkFoldersIsSet = _fullFrameworkFolders.Length > 0; bool fullFrameworkTableLocationsIsSet = FullFrameworkAssemblyTables.Length > 0; bool profileIsSet = profileNameIsSet && (fullFrameworkFoldersIsSet || fullFrameworkTableLocationsIsSet); @@ -2942,7 +3049,7 @@ private void DumpTargetProfileLists(AssemblyTableInfo[] installedAssemblyTableIn return; } - string dumpFrameworkSubsetList = Environment.GetEnvironmentVariable("MSBUILDDUMPFRAMEWORKSUBSETLIST"); + string dumpFrameworkSubsetList = TaskEnvironment.GetEnvironmentVariable("MSBUILDDUMPFRAMEWORKSUBSETLIST"); if (dumpFrameworkSubsetList == null) { return; @@ -2955,7 +3062,7 @@ private void DumpTargetProfileLists(AssemblyTableInfo[] installedAssemblyTableIn { if (redistInfo != null) { - Log.LogMessage(MessageImportance.Low, Strings.FormattedAssemblyInfo, redistInfo.Path); + Log.LogMessage(MessageImportance.Low, Strings.FormattedAssemblyInfo, redistInfo.Path.OriginalValue); } } @@ -2966,7 +3073,7 @@ private void DumpTargetProfileLists(AssemblyTableInfo[] installedAssemblyTableIn { if (inclusionListInfo != null) { - Log.LogMessage(MessageImportance.Low, Strings.FormattedAssemblyInfo, inclusionListInfo.Path); + Log.LogMessage(MessageImportance.Low, Strings.FormattedAssemblyInfo, inclusionListInfo.Path.OriginalValue); } } } @@ -3084,7 +3191,7 @@ private AssemblyTableInfo[] GetInstalledAssemblyTableInfo(bool ignoreInstalledAs string[] listPaths = GetAssemblyListPaths(targetFrameworkDirectory); foreach (string listPath in listPaths) { - tableMap[listPath] = new AssemblyTableInfo(listPath, targetFrameworkDirectory); + tableMap[listPath] = AssemblyTableInfo.CreateFromRelativePath(listPath, targetFrameworkDirectory, TaskEnvironment, Log); } } } @@ -3108,7 +3215,7 @@ private AssemblyTableInfo[] GetInstalledAssemblyTableInfo(bool ignoreInstalledAs } } - tableMap[installedAssemblyTable.ItemSpec] = new AssemblyTableInfo(installedAssemblyTable.ItemSpec, frameworkDirectory); + tableMap[installedAssemblyTable.ItemSpec] = AssemblyTableInfo.CreateFromRelativePath(installedAssemblyTable.ItemSpec, frameworkDirectory, TaskEnvironment, Log); } AssemblyTableInfo[] extensions = new AssemblyTableInfo[tableMap.Count]; @@ -3249,7 +3356,7 @@ internal static SystemProcessorArchitecture TargetProcessorArchitectureToEnumera private string GetAssemblyPathInGac(AssemblyNameExtension assemblyName, SystemProcessorArchitecture targetProcessorArchitecture, GetAssemblyRuntimeVersion getRuntimeVersion, Version targetedRuntimeVersion, FileExists fileExists, bool fullFusionName, bool specificVersion) { #if FEATURE_GAC - return GlobalAssemblyCache.GetLocation(BuildEngine as IBuildEngine4, assemblyName, targetProcessorArchitecture, getRuntimeVersion, targetedRuntimeVersion, fullFusionName, fileExists, null, null, specificVersion /* this value does not matter if we are passing a full fusion name*/); + return GlobalAssemblyCache.GetLocation(BuildEngine as IBuildEngine4, assemblyName, targetProcessorArchitecture, getRuntimeVersion, targetedRuntimeVersion, fullFusionName, fileExists, null, null, specificVersion /* this value does not matter if we are passing a full fusion name*/, TaskEnvironment); #else return string.Empty; #endif @@ -3274,9 +3381,9 @@ public override bool Execute() // FilesWritten already defines a public setter which no-ops. Changing its visiblity is a breaking // change, so we can't set it outside of RAR when we check for properties with OutputAttribute. // It only has two possible states, so we can just compute it here. - if (_stateFile != null && FileUtilities.FileExistsNoThrow(_stateFile)) + if (_stateFile.Value is not null && FileUtilities.FileExistsNoThrow(_stateFile.Value)) { - _filesWritten.Add(new TaskItem(_stateFile)); + _filesWritten.Add(new TaskItem(_stateFile.OriginalValue)); } return success; diff --git a/src/Tasks/AssemblyDependency/Resolver.cs b/src/Tasks/AssemblyDependency/Resolver.cs index 62a455c983d..1f134ffe56f 100644 --- a/src/Tasks/AssemblyDependency/Resolver.cs +++ b/src/Tasks/AssemblyDependency/Resolver.cs @@ -52,10 +52,15 @@ internal abstract class Resolver /// protected bool compareProcessorArchitecture; + /// + /// TaskEnvironment for thread-safe environment variable access and path resolution. + /// + protected TaskEnvironment taskEnvironment; + /// /// Construct. /// - protected Resolver(string searchPathElement, GetAssemblyName getAssemblyName, FileExists fileExists, GetAssemblyRuntimeVersion getRuntimeVersion, Version targetedRuntimeVersion, ProcessorArchitecture targetedProcessorArchitecture, bool compareProcessorArchitecture) + protected Resolver(string searchPathElement, GetAssemblyName getAssemblyName, FileExists fileExists, GetAssemblyRuntimeVersion getRuntimeVersion, Version targetedRuntimeVersion, ProcessorArchitecture targetedProcessorArchitecture, bool compareProcessorArchitecture, TaskEnvironment taskEnvironment) { this.searchPathElement = searchPathElement; this.getAssemblyName = getAssemblyName; @@ -64,6 +69,7 @@ protected Resolver(string searchPathElement, GetAssemblyName getAssemblyName, Fi this.targetedRuntimeVersion = targetedRuntimeVersion; this.targetProcessorArchitecture = targetedProcessorArchitecture; this.compareProcessorArchitecture = compareProcessorArchitecture; + this.taskEnvironment = taskEnvironment; } /// @@ -141,25 +147,25 @@ protected bool ResolveAsFile( /// True if this is a primary reference directly from the project file. /// Whether the version needs to match exactly or loosely. /// Whether to allow naming mismatch. - /// Path to a possible file. + /// Full path to a possible file. /// Information about why the candidate file didn't match protected bool FileMatchesAssemblyName( AssemblyNameExtension assemblyName, bool isPrimaryProjectReference, bool wantSpecificVersion, bool allowMismatchBetweenFusionNameAndFileName, - string pathToCandidateAssembly, + string fullPathToCandidateAssembly, ResolutionSearchLocation searchLocation) { if (searchLocation != null) { - searchLocation.FileNameAttempted = pathToCandidateAssembly; + searchLocation.FileNameAttempted = fullPathToCandidateAssembly; } // Base name of the target file has to match the Name from the assemblyName if (!allowMismatchBetweenFusionNameAndFileName) { - string candidateBaseName = Path.GetFileNameWithoutExtension(pathToCandidateAssembly); + string candidateBaseName = Path.GetFileNameWithoutExtension(fullPathToCandidateAssembly); if (!String.Equals(assemblyName?.Name, candidateBaseName, StringComparison.CurrentCultureIgnoreCase)) { if (searchLocation != null) @@ -180,7 +186,7 @@ protected bool FileMatchesAssemblyName( bool isSimpleAssemblyName = assemblyName?.IsSimpleName == true; - if (fileExists(pathToCandidateAssembly)) + if (fileExists(fullPathToCandidateAssembly)) { // 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. @@ -203,7 +209,7 @@ protected bool FileMatchesAssemblyName( AssemblyNameExtension targetAssemblyName = null; try { - targetAssemblyName = getAssemblyName(pathToCandidateAssembly); + targetAssemblyName = getAssemblyName(fullPathToCandidateAssembly); } catch (FileLoadException) { @@ -293,7 +299,7 @@ protected bool FileMatchesAssemblyName( /// True if this is a primary reference directly from the project file. /// Whether an exact version match is requested. /// The possible filename extensions of the assembly. Must be one of these or its no match. - /// the directory to look in + /// Absolute path to the directory to look in. May not be in canonical form. /// Receives the list of locations that this function tried to find the assembly. May be "null". /// 'null' if the assembly wasn't found. protected string ResolveFromDirectory( @@ -301,7 +307,7 @@ protected string ResolveFromDirectory( bool isPrimaryProjectReference, bool wantSpecificVersion, string[] executableExtensions, - string directory, + string fullPathToDirectory, List assembliesConsideredAndRejected) { if (assemblyName == null) @@ -313,7 +319,7 @@ protected string ResolveFromDirectory( // 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) + if (fullPathToDirectory != null) { string weakNameBase = assemblyName.Name; foreach (string executableExtension in executableExtensions) @@ -323,12 +329,12 @@ protected string ResolveFromDirectory( try { - fullPath = Path.Combine(directory, baseName); + fullPath = Path.Combine(fullPathToDirectory, baseName); } catch (Exception e) when (ExceptionHandling.IsIoRelatedException(e)) { // 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); + throw new InvalidParameterValueException("SearchPaths", fullPathToDirectory + (fullPathToDirectory.EndsWith("\\", StringComparison.OrdinalIgnoreCase) ? String.Empty : "\\") + baseName, e.Message); } // We have a full path returned @@ -379,7 +385,7 @@ protected string ResolveFromDirectory( { if (String.Equals(executableExtension, weakNameBaseExtension, StringComparison.CurrentCultureIgnoreCase)) { - string fullPath = Path.Combine(directory, weakNameBase); + string fullPath = Path.Combine(fullPathToDirectory, weakNameBase); var extensionlessAssemblyName = new AssemblyNameExtension(weakNameBaseFileName); if (ResolveAsFile(fullPath, extensionlessAssemblyName, isPrimaryProjectReference, wantSpecificVersion, false, assembliesConsideredAndRejected)) diff --git a/src/Tasks/GetReferenceAssemblyPaths.cs b/src/Tasks/GetReferenceAssemblyPaths.cs index fd2cf698a3e..c2a7f5c50a4 100644 --- a/src/Tasks/GetReferenceAssemblyPaths.cs +++ b/src/Tasks/GetReferenceAssemblyPaths.cs @@ -50,7 +50,8 @@ public class GetReferenceAssemblyPaths : TaskExtension, IMultiThreadableTask new FileExists(p => FileUtilities.FileExistsNoThrow(p)), GlobalAssemblyCache.pathFromFusionName, GlobalAssemblyCache.gacEnumerator, - false); + false, + TaskEnvironment.Fallback); return !string.IsNullOrEmpty(path); }, LazyThreadSafetyMode.PublicationOnly); @@ -307,4 +308,4 @@ private IList ResolveAbsoluteFallbackSearchPaths(string fallbackSe #endregion } -} +} \ No newline at end of file diff --git a/src/Tasks/InstalledSDKResolver.cs b/src/Tasks/InstalledSDKResolver.cs index b8f7d6a0b12..631f631b808 100644 --- a/src/Tasks/InstalledSDKResolver.cs +++ b/src/Tasks/InstalledSDKResolver.cs @@ -24,8 +24,8 @@ internal class InstalledSDKResolver : Resolver /// /// 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) + public InstalledSDKResolver(Dictionary resolvedSDKs, string searchPathElement, GetAssemblyName getAssemblyName, FileExists fileExists, GetAssemblyRuntimeVersion getRuntimeVersion, Version targetedRuntimeVesion, TaskEnvironment taskEnvironment) + : base(searchPathElement, getAssemblyName, fileExists, getRuntimeVersion, targetedRuntimeVesion, System.Reflection.ProcessorArchitecture.None, false, taskEnvironment) { _resolvedSDKs = resolvedSDKs; } @@ -53,7 +53,7 @@ public override bool Resolve( // We have found a resolved SDK item that matches the one on the reference items. if (_resolvedSDKs.TryGetValue(sdkName, out ITaskItem resolvedSDK)) { - string sdkDirectory = resolvedSDK.ItemSpec; + string sdkDirectory = taskEnvironment.GetAbsolutePath(resolvedSDK.ItemSpec).Value; string configuration = resolvedSDK.GetMetadata("TargetedSDKConfiguration"); string architecture = resolvedSDK.GetMetadata("TargetedSDKArchitecture"); diff --git a/src/Tasks/RedistList.cs b/src/Tasks/RedistList.cs index 91fc81d0921..e549403c2f4 100644 --- a/src/Tasks/RedistList.cs +++ b/src/Tasks/RedistList.cs @@ -621,8 +621,8 @@ internal Dictionary GenerateDenyList(AssemblyTableInfo[] allowLi if (allowListErrors.Count == errorsBeforeReadCall) { // The allowList errors passes back problems reading the redist file through the use of an array containing exceptions - allowListErrors.Add(new Exception(ResourceUtilities.FormatResourceStringStripCodeAndKeyword("ResolveAssemblyReference.NoSubSetRedistListName", info.Path))); - allowListErrorFileNames.Add(info.Path); + allowListErrors.Add(new Exception(ResourceUtilities.FormatResourceStringStripCodeAndKeyword("ResolveAssemblyReference.NoSubSetRedistListName", info.Path.OriginalValue))); + allowListErrorFileNames.Add(info.Path.OriginalValue); } } } @@ -689,7 +689,7 @@ internal Dictionary GenerateDenyList(AssemblyTableInfo[] allowLi /// 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 path = assemblyTableInfo.Path.Value; string redistName = null; XmlReader reader = null; @@ -978,17 +978,36 @@ internal class AssemblyTableInfo : IComparable { private string _descriptor; - internal AssemblyTableInfo(string path, string frameworkDirectory) + /// + /// Creates an from a potentially relative path, + /// absolutizing it and canonicalizing it if possible using the provided . + /// + /// Path to the assembly table file (can be relative). + /// Framework directory path. + /// TaskEnvironment for path conversion. + /// Logger for diagnostic messages when canonicalization fails. + internal static AssemblyTableInfo CreateFromRelativePath(string path, string frameworkDirectory, TaskEnvironment taskEnvironment, TaskLoggingHelper log) + { + AbsolutePath canonicalPath = taskEnvironment.GetAbsolutePath(FileUtilities.NormalizeForPathComparison(path)).TryGetCanonicalForm(log); + return new AssemblyTableInfo(canonicalPath, FileUtilities.NormalizeForPathComparison(frameworkDirectory)); + } + + /// + /// Constructor that expects absolute paths. Use this when paths are already fully qualified. + /// + /// Absolute path to the assembly table file + /// Framework directory path + internal AssemblyTableInfo(string absolutePath, string frameworkDirectory) { - Path = FileUtilities.NormalizeForPathComparison(path); + Path = new AbsolutePath(FileUtilities.NormalizeForPathComparison(absolutePath)); FrameworkDirectory = FileUtilities.NormalizeForPathComparison(frameworkDirectory); } - internal string Path { get; } + internal AbsolutePath Path { get; } internal string FrameworkDirectory { get; } - internal string Descriptor => _descriptor ?? (_descriptor = Path + FrameworkDirectory); + internal string Descriptor => _descriptor ?? (_descriptor = Path.Value + FrameworkDirectory); public int CompareTo(object obj) { diff --git a/src/Tasks/Resources/Strings.resx b/src/Tasks/Resources/Strings.resx index fb697866cd8..36b081a1e5a 100644 --- a/src/Tasks/Resources/Strings.resx +++ b/src/Tasks/Resources/Strings.resx @@ -500,6 +500,9 @@ Expected file "{0}" does not exist. + + Could not canonicalize path "{0}": {1}. The path will be used as-is. + MSB3082: Task failed because "{0}" was not found, or the .NET Framework {1} is not installed. Please install the .NET Framework {1}. {StrBegin="MSB3082: "} diff --git a/src/Tasks/Resources/xlf/Strings.cs.xlf b/src/Tasks/Resources/xlf/Strings.cs.xlf index dde273ca0ae..8763eda1d85 100644 --- a/src/Tasks/Resources/xlf/Strings.cs.xlf +++ b/src/Tasks/Resources/xlf/Strings.cs.xlf @@ -619,6 +619,11 @@ Očekávaný soubor {0} neexistuje. + + Could not canonicalize path "{0}": {1}. The path will be used as-is. + Could not canonicalize path "{0}": {1}. The path will be used as-is. + + MSB3082: Task failed because "{0}" was not found, or the .NET Framework {1} is not installed. Please install the .NET Framework {1}. MSB3082: Úloha se nezdařila, protože nebyla nalezena položka {0} nebo není nainstalováno rozhraní .NET Framework {1}. Nainstalujte rozhraní .NET Framework {1}. diff --git a/src/Tasks/Resources/xlf/Strings.de.xlf b/src/Tasks/Resources/xlf/Strings.de.xlf index 99732a18ae6..2e11f7817cf 100644 --- a/src/Tasks/Resources/xlf/Strings.de.xlf +++ b/src/Tasks/Resources/xlf/Strings.de.xlf @@ -619,6 +619,11 @@ Die erwartete Datei "{0}" ist nicht vorhanden. + + Could not canonicalize path "{0}": {1}. The path will be used as-is. + Could not canonicalize path "{0}": {1}. The path will be used as-is. + + MSB3082: Task failed because "{0}" was not found, or the .NET Framework {1} is not installed. Please install the .NET Framework {1}. MSB3082: Fehler bei der Aufgabe. "{0}" wurde nicht gefunden, oder .NET Framework {1} ist nicht installiert. Installieren Sie .NET Framework {1}. diff --git a/src/Tasks/Resources/xlf/Strings.es.xlf b/src/Tasks/Resources/xlf/Strings.es.xlf index 2ba711a7b04..f90964da4d9 100644 --- a/src/Tasks/Resources/xlf/Strings.es.xlf +++ b/src/Tasks/Resources/xlf/Strings.es.xlf @@ -619,6 +619,11 @@ El archivo esperado "{0}" no existe. + + Could not canonicalize path "{0}": {1}. The path will be used as-is. + Could not canonicalize path "{0}": {1}. The path will be used as-is. + + MSB3082: Task failed because "{0}" was not found, or the .NET Framework {1} is not installed. Please install the .NET Framework {1}. MSB3082: Error en la tarea porque no se encuentra "{0}" o .NET Framework {1} no está instalado. Instale .NET Framework {1}. diff --git a/src/Tasks/Resources/xlf/Strings.fr.xlf b/src/Tasks/Resources/xlf/Strings.fr.xlf index a1bbc4343c5..fdbd95833c2 100644 --- a/src/Tasks/Resources/xlf/Strings.fr.xlf +++ b/src/Tasks/Resources/xlf/Strings.fr.xlf @@ -619,6 +619,11 @@ Le fichier attendu "{0}" n'existe pas. + + Could not canonicalize path "{0}": {1}. The path will be used as-is. + Could not canonicalize path "{0}": {1}. The path will be used as-is. + + MSB3082: Task failed because "{0}" was not found, or the .NET Framework {1} is not installed. Please install the .NET Framework {1}. MSB3082: La tâche a échoué, car "{0}" est introuvable ou le .NET Framework {1} n'est pas installé. Installez le .NET Framework {1}. diff --git a/src/Tasks/Resources/xlf/Strings.it.xlf b/src/Tasks/Resources/xlf/Strings.it.xlf index b5aba491c4a..80e39029e09 100644 --- a/src/Tasks/Resources/xlf/Strings.it.xlf +++ b/src/Tasks/Resources/xlf/Strings.it.xlf @@ -619,6 +619,11 @@ Il file previsto "{0}" non esiste. + + Could not canonicalize path "{0}": {1}. The path will be used as-is. + Could not canonicalize path "{0}": {1}. The path will be used as-is. + + MSB3082: Task failed because "{0}" was not found, or the .NET Framework {1} is not installed. Please install the .NET Framework {1}. MSB3082: l'attività non è riuscita perché "{0}" non è stato trovato oppure perché .NET Framework {1} non è installato. Installare .NET Framework {1}. diff --git a/src/Tasks/Resources/xlf/Strings.ja.xlf b/src/Tasks/Resources/xlf/Strings.ja.xlf index 1c9a490b7e7..ed660e92eb6 100644 --- a/src/Tasks/Resources/xlf/Strings.ja.xlf +++ b/src/Tasks/Resources/xlf/Strings.ja.xlf @@ -619,6 +619,11 @@ 指定されたファイル "{0}" は存在しません。 + + Could not canonicalize path "{0}": {1}. The path will be used as-is. + Could not canonicalize path "{0}": {1}. The path will be used as-is. + + MSB3082: Task failed because "{0}" was not found, or the .NET Framework {1} is not installed. Please install the .NET Framework {1}. MSB3082: "{0}" が見つからなかったため、または .NET Framework {1} がインストールされていないため、タスクに失敗しました。.NET Framework {1} をインストールしてください。 diff --git a/src/Tasks/Resources/xlf/Strings.ko.xlf b/src/Tasks/Resources/xlf/Strings.ko.xlf index 5a2d828cb01..d59ef01ca32 100644 --- a/src/Tasks/Resources/xlf/Strings.ko.xlf +++ b/src/Tasks/Resources/xlf/Strings.ko.xlf @@ -619,6 +619,11 @@ 필요한 "{0}" 파일이 없습니다. + + Could not canonicalize path "{0}": {1}. The path will be used as-is. + Could not canonicalize path "{0}": {1}. The path will be used as-is. + + MSB3082: Task failed because "{0}" was not found, or the .NET Framework {1} is not installed. Please install the .NET Framework {1}. MSB3082: "{0}"이(가) 없거나 .NET Framework {1}이(가) 설치되어 있지 않아 작업을 수행하지 못했습니다. .NET Framework {1}을(를) 설치하세요. diff --git a/src/Tasks/Resources/xlf/Strings.pl.xlf b/src/Tasks/Resources/xlf/Strings.pl.xlf index 8e8b2953fcb..d8e4a416b28 100644 --- a/src/Tasks/Resources/xlf/Strings.pl.xlf +++ b/src/Tasks/Resources/xlf/Strings.pl.xlf @@ -619,6 +619,11 @@ Oczekiwany plik „{0}” nie istnieje. + + Could not canonicalize path "{0}": {1}. The path will be used as-is. + Could not canonicalize path "{0}": {1}. The path will be used as-is. + + MSB3082: Task failed because "{0}" was not found, or the .NET Framework {1} is not installed. Please install the .NET Framework {1}. MSB3082: Zadanie zakończyło się niepowodzeniem, ponieważ nie można odnaleźć „{0}” lub program .NET Framework {1} nie jest zainstalowany. Zainstaluj program .NET Framework {1}. diff --git a/src/Tasks/Resources/xlf/Strings.pt-BR.xlf b/src/Tasks/Resources/xlf/Strings.pt-BR.xlf index bdbc7054eee..ab704580f70 100644 --- a/src/Tasks/Resources/xlf/Strings.pt-BR.xlf +++ b/src/Tasks/Resources/xlf/Strings.pt-BR.xlf @@ -619,6 +619,11 @@ O arquivo esperado "{0}" não existe. + + Could not canonicalize path "{0}": {1}. The path will be used as-is. + Could not canonicalize path "{0}": {1}. The path will be used as-is. + + MSB3082: Task failed because "{0}" was not found, or the .NET Framework {1} is not installed. Please install the .NET Framework {1}. MSB3082: Falha na tarefa porque "{0}" não foi encontrado ou o .NET Framework {1} não está instalado. Instale o .NET Framework {1}. diff --git a/src/Tasks/Resources/xlf/Strings.ru.xlf b/src/Tasks/Resources/xlf/Strings.ru.xlf index f5fedc7bcbe..8b4dc9b0f24 100644 --- a/src/Tasks/Resources/xlf/Strings.ru.xlf +++ b/src/Tasks/Resources/xlf/Strings.ru.xlf @@ -619,6 +619,11 @@ Ожидаемый файл "{0}" не существует. + + Could not canonicalize path "{0}": {1}. The path will be used as-is. + Could not canonicalize path "{0}": {1}. The path will be used as-is. + + MSB3082: Task failed because "{0}" was not found, or the .NET Framework {1} is not installed. Please install the .NET Framework {1}. MSB3082: задача не выполнена, так как не найден "{0}" или не установлена платформа .NET Framework {1}. Установите .NET Framework {1}. diff --git a/src/Tasks/Resources/xlf/Strings.tr.xlf b/src/Tasks/Resources/xlf/Strings.tr.xlf index 88671129bf6..f9aa3b163ce 100644 --- a/src/Tasks/Resources/xlf/Strings.tr.xlf +++ b/src/Tasks/Resources/xlf/Strings.tr.xlf @@ -619,6 +619,11 @@ Beklenen "{0}" dosyası yok. + + Could not canonicalize path "{0}": {1}. The path will be used as-is. + Could not canonicalize path "{0}": {1}. The path will be used as-is. + + MSB3082: Task failed because "{0}" was not found, or the .NET Framework {1} is not installed. Please install the .NET Framework {1}. MSB3082: "{0}" bulunamadığından veya .NET Framework {1} yüklü olmadığından görev başarısız oldu. Lütfen .NET Framework {1} yükleyin. diff --git a/src/Tasks/Resources/xlf/Strings.zh-Hans.xlf b/src/Tasks/Resources/xlf/Strings.zh-Hans.xlf index 85c481533a7..a6d4a4b1a3c 100644 --- a/src/Tasks/Resources/xlf/Strings.zh-Hans.xlf +++ b/src/Tasks/Resources/xlf/Strings.zh-Hans.xlf @@ -619,6 +619,11 @@ 所需文件“{0}”不存在。 + + Could not canonicalize path "{0}": {1}. The path will be used as-is. + Could not canonicalize path "{0}": {1}. The path will be used as-is. + + MSB3082: Task failed because "{0}" was not found, or the .NET Framework {1} is not installed. Please install the .NET Framework {1}. MSB3082: 任务失败,因为未找到“{0}”,或者未安装 .NET Framework {1}。请安装 .NET Framework {1}。 diff --git a/src/Tasks/Resources/xlf/Strings.zh-Hant.xlf b/src/Tasks/Resources/xlf/Strings.zh-Hant.xlf index f5241dfc562..d62791bd214 100644 --- a/src/Tasks/Resources/xlf/Strings.zh-Hant.xlf +++ b/src/Tasks/Resources/xlf/Strings.zh-Hant.xlf @@ -619,6 +619,11 @@ 預期的檔案 "{0}" 不存在。 + + Could not canonicalize path "{0}": {1}. The path will be used as-is. + Could not canonicalize path "{0}": {1}. The path will be used as-is. + + MSB3082: Task failed because "{0}" was not found, or the .NET Framework {1} is not installed. Please install the .NET Framework {1}. MSB3082: 工作失敗,因為找不到 "{0}" 或者未安裝 .NET Framework {1}。請安裝 .NET Framework {1}。 diff --git a/src/Tasks/StateFileBase.cs b/src/Tasks/StateFileBase.cs index 01e2a102f06..5ec192f9964 100644 --- a/src/Tasks/StateFileBase.cs +++ b/src/Tasks/StateFileBase.cs @@ -29,14 +29,31 @@ internal abstract class StateFileBase private byte _serializedVersion = CurrentSerializationVersion; /// - /// True if should create the state file and serialize ourselves, false otherwise. + /// True if should create the state file and serialize ourselves, false otherwise. /// internal virtual bool HasStateToSave => true; /// /// Writes the contents of this object out to the specified file. /// + /// + /// Prioritize using the AbsolutePath overload of this method. This method is still used by unenlightened tasks, but new code should use the AbsolutePath overload. + /// Delete this method once all tasks have been migrated to the AbsolutePath overload. + /// internal virtual void SerializeCache(string stateFile, TaskLoggingHelper log, bool serializeEmptyState = false) + { + if (string.IsNullOrEmpty(stateFile)) + { + return; + } + + SerializeCache(new AbsolutePath(stateFile, ignoreRootedCheck: true), log, serializeEmptyState); + } + + /// + /// Writes the contents of this object out to the specified file. + /// + internal virtual void SerializeCache(AbsolutePath stateFile, TaskLoggingHelper log, bool serializeEmptyState = false) { try { @@ -64,7 +81,7 @@ internal virtual void SerializeCache(string stateFile, TaskLoggingHelper log, bo { // 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); + log.LogWarningWithCodeFromResources("General.CouldNotWriteStateFile", stateFile.OriginalValue, e.Message); } } @@ -72,8 +89,25 @@ internal virtual void SerializeCache(string stateFile, TaskLoggingHelper log, bo /// /// Reads the specified file from disk into a StateFileBase derived object. + /// stateFile should be absolute path to the file on disk. /// + /// + /// Prioritize using the AbsolutePath overload of this method. This method is still used by unenlightened tasks, but new code should use the AbsolutePath overload. + /// internal static T DeserializeCache(string stateFile, TaskLoggingHelper log) where T : StateFileBase + { + if (string.IsNullOrEmpty(stateFile)) + { + return null; + } + + return DeserializeCache(new AbsolutePath(stateFile, ignoreRootedCheck: true), log); + } + + /// + /// Reads the specified file from disk into a StateFileBase derived object. + /// + internal static T DeserializeCache(AbsolutePath stateFile, TaskLoggingHelper log) where T : StateFileBase { T retVal = null; @@ -92,7 +126,7 @@ internal static T DeserializeCache(string stateFile, TaskLoggingHelper log) w // For the latter case, internals may be unexpectedly null. if (version != CurrentSerializationVersion) { - log.LogMessageFromResources("General.CouldNotReadStateFileMessage", stateFile, log.FormatResourceString("General.IncompatibleStateFileType")); + log.LogMessageFromResources("General.CouldNotReadStateFileMessage", stateFile.OriginalValue, log.FormatResourceString("General.IncompatibleStateFileType")); return null; } @@ -108,7 +142,7 @@ internal static T DeserializeCache(string stateFile, TaskLoggingHelper log) w if (retVal == null) { - log.LogMessageFromResources("General.CouldNotReadStateFileMessage", stateFile, + log.LogMessageFromResources("General.CouldNotReadStateFileMessage", stateFile.OriginalValue, log.FormatResourceString("General.IncompatibleStateFileType")); } } @@ -120,12 +154,13 @@ internal static T DeserializeCache(string stateFile, TaskLoggingHelper log) w // 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.LogMessageFromResources("General.CouldNotReadStateFileMessage", stateFile, e.Message); + log.LogMessageFromResources("General.CouldNotReadStateFileMessage", stateFile.OriginalValue, e.Message); } return retVal; } + /// /// Deletes the state file from disk /// diff --git a/src/Tasks/SystemState.cs b/src/Tasks/SystemState.cs index 37cf4cbf269..ebb88501e8b 100644 --- a/src/Tasks/SystemState.cs +++ b/src/Tasks/SystemState.cs @@ -556,11 +556,12 @@ private void GetAssemblyMetadata( /// /// Reads in cached data from stateFiles to build an initial cache. Avoids logging warnings or errors. /// - /// List of locations of caches on disk. + /// List of locations of caches on disk. /// How to log /// Whether a file exists + /// TaskEnvironment for path resolution /// A cache representing key aspects of file states. - internal static SystemState DeserializePrecomputedCaches(ITaskItem[] stateFiles, TaskLoggingHelper log, FileExists fileExists) + internal static SystemState DeserializePrecomputedCaches(ITaskItem[] stateFiles, TaskLoggingHelper log, FileExists fileExists, TaskEnvironment taskEnvironment) { SystemState retVal = new SystemState(); retVal.isDirty = stateFiles.Length > 0; @@ -568,8 +569,25 @@ internal static SystemState DeserializePrecomputedCaches(ITaskItem[] stateFiles, foreach (ITaskItem stateFile in stateFiles) { - // Verify that it's a real stateFile. Log message but do not error if not. - SystemState sysState = DeserializeCache(stateFile.ToString(), log); + SystemState sysState = null; + string stateFilePath = null; + if (ChangeWaves.AreFeaturesEnabled(ChangeWaves.Wave18_8)) + { + AbsolutePath stateFileAbsolutePath = taskEnvironment.GetAbsolutePath(stateFile.ItemSpec); + stateFilePath = stateFileAbsolutePath.Value; + + // Verify that it's a real stateFile. Log message but do not error if not. + sysState = DeserializeCache(stateFileAbsolutePath, log); + } + else + { + // This should be equivalent to stateFile.ItemSpec, but in some cases (for example custom TaskItems) it might not be. + stateFilePath = stateFile.ToString(); + + // Verify that it's a real stateFile. Log message but do not error if not. + sysState = DeserializeCache(stateFilePath, log); + } + if (sysState == null) { continue; @@ -580,7 +598,8 @@ internal static SystemState DeserializePrecomputedCaches(ITaskItem[] stateFiles, if (!assembliesFound.Contains(relativePath)) { FileState fileState = kvp.Value; - string fullPath = Path.GetFullPath(Path.Combine(Path.GetDirectoryName(stateFile.ToString()), relativePath)); + string fullPath = taskEnvironment.GetAbsolutePath(Path.Combine(Path.GetDirectoryName(stateFilePath), relativePath)).GetCanonicalForm().Value; + if (fileExists(fullPath)) { // Correct file path @@ -599,7 +618,7 @@ internal static SystemState DeserializePrecomputedCaches(ITaskItem[] stateFiles, /// /// Path to which to write the precomputed cache /// How to log - internal void SerializePrecomputedCache(string stateFile, TaskLoggingHelper log) + internal void SerializePrecomputedCache(AbsolutePath stateFile, TaskLoggingHelper log) { // Save a copy of instanceLocalOutgoingFileStateCache so we can restore it later. SerializeCacheByTranslator serializes // instanceLocalOutgoingFileStateCache by default, so change that to the relativized form, then change it back. @@ -610,7 +629,7 @@ internal void SerializePrecomputedCache(string stateFile, TaskLoggingHelper log) { if (FileUtilities.FileExistsNoThrow(stateFile)) { - log.LogWarningWithCodeFromResources("General.StateFileAlreadyPresent", stateFile); + log.LogWarningWithCodeFromResources("General.StateFileAlreadyPresent", stateFile.OriginalValue); } SerializeCache(stateFile, log); } diff --git a/src/Tasks/TaskEnvironmentExtensions.cs b/src/Tasks/TaskEnvironmentExtensions.cs index bd3d23220b7..631765726f2 100644 --- a/src/Tasks/TaskEnvironmentExtensions.cs +++ b/src/Tasks/TaskEnvironmentExtensions.cs @@ -1,7 +1,9 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System; using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; namespace Microsoft.Build.Tasks { @@ -10,6 +12,28 @@ namespace Microsoft.Build.Tasks /// internal static class TaskEnvironmentExtensions { + /// + /// Tries to return the canonical form of an (resolving ".." segments, etc.). + /// on .NET Framework validates path characters and throws + /// for illegal characters (e.g. |, <, >). + /// .NET Core is more permissive and delegates character validation to the OS. + /// When canonicalization fails, the original absolute path is returned as-is. + /// + /// The absolute path to canonicalize. + /// Optional logger. When provided, a low-importance diagnostic message is logged on failure. + internal static AbsolutePath TryGetCanonicalForm(this AbsolutePath absolutePath, TaskLoggingHelper? log = null) + { + try + { + return absolutePath.GetCanonicalForm(); + } + catch (Exception e) + { + log?.LogMessageFromResources(MessageImportance.Low, "General.FailedToCanonicalizePath", absolutePath.Value, e.Message); + return absolutePath; + } + } + /// /// Absolutizes each non-empty path in the array using . /// Returns if is .