diff --git a/documentation/wiki/ChangeWaves.md b/documentation/wiki/ChangeWaves.md index 9662b81665d..2ca32f08433 100644 --- a/documentation/wiki/ChangeWaves.md +++ b/documentation/wiki/ChangeWaves.md @@ -29,6 +29,9 @@ Change wave checks around features will be removed in the release that accompani ## Current Rotation of Change Waves +### 18.8 +- [RAR task: across multiple input properties, resolve relative paths against the project directory (not the process current directory)](https://github.com/dotnet/msbuild/pull/13319) + ### 18.7 - [Copy task retries on ERROR_ACCESS_DENIED on non-Windows platforms to handle transient lock conflicts (e.g. macOS CoW filesystems)](https://github.com/dotnet/msbuild/issues/13463) - [Fix ASP.NET WebSite projects to resolve netstandard2.0 dependencies](https://github.com/dotnet/msbuild/pull/13058) - Pass TargetFrameworkVersion to RAR task and copy netstandard.dll facade for .NET Framework 4.7.1+ web projects. 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/Framework/TaskEnvironment.cs b/src/Framework/TaskEnvironment.cs index 43f3c92315e..5e7fe7da837 100644 --- a/src/Framework/TaskEnvironment.cs +++ b/src/Framework/TaskEnvironment.cs @@ -33,6 +33,13 @@ internal TaskEnvironment(ITaskEnvironmentDriver driver) /// public static TaskEnvironment Fallback { get; } = new(MultiProcessTaskEnvironmentDriver.Instance); + /// + /// Gets a value indicating whether this is providing + /// per-task isolated state (multithreaded mode). When , the + /// environment delegates to the shared process environment (multi-process mode). + /// + internal bool IsMultiThreaded => _driver is MultiThreadedTaskEnvironmentDriver; + /// /// Creates a new with isolated working directory and environment variables. /// 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/Miscellaneous.cs b/src/Tasks.UnitTests/AssemblyDependency/Miscellaneous.cs index 0637a748cce..9a104ec01d4 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/ResolveAssemblyReferenceCacheSerialization.cs b/src/Tasks.UnitTests/AssemblyDependency/ResolveAssemblyReferenceCacheSerialization.cs index 641a8164086..7b40c558219 100644 --- a/src/Tasks.UnitTests/AssemblyDependency/ResolveAssemblyReferenceCacheSerialization.cs +++ b/src/Tasks.UnitTests/AssemblyDependency/ResolveAssemblyReferenceCacheSerialization.cs @@ -192,5 +192,97 @@ public void OutgoingCacheIsEmpty() // The new cache was not written to disk at all because none of the entries were actually used. sysState2.ShouldBeNull(); } + + /// + /// SystemState.DeserializePrecomputedCaches absolutizes the state file path via TaskEnvironment + /// (relative to the project directory), not via the process current working directory. + /// A relative state file path that exists in CWD but not in the project directory should + /// fail to deserialize. + /// + [Fact] + public void DeserializePrecomputedCaches_AbsolutizesStateFilePathViaTaskEnvironment() + { + using TestEnvironment env = TestEnvironment.Create(); + + // cacheDir holds the real cache file (and becomes CWD); projectDir is empty. + string cacheDir = env.CreateFolder().Path; + string projectDir = env.CreateFolder().Path; + string cacheFileName = "rar.cache"; + string cacheFullPath = Path.Combine(cacheDir, cacheFileName); + + WriteCacheFileWithSingleEntry(cacheFullPath, "TestAssembly.dll"); + + env.SetCurrentDirectory(cacheDir); + + ITaskItem[] stateFiles = [new TaskItem(cacheFileName)]; + TaskEnvironment taskEnvironment = TaskEnvironment.CreateWithProjectDirectoryAndEnvironment(projectDir); + + SystemState result = SystemState.DeserializePrecomputedCaches( + stateFiles, + null, + _ => true, + taskEnvironment); + + // The relative path is resolved via TaskEnvironment to projectDir/rar.cache, which doesn't + // exist. If the implementation incorrectly used CWD, it would have found the cache and + // populated entries. + result.ShouldNotBeNull(); + result.instanceLocalFileStateCache.ShouldBeEmpty(); + } + + /// + /// With Wave18_8 disabled, SystemState.DeserializePrecomputedCaches uses the raw + /// stateFile.ToString() path which means a relative state file path is opened relative + /// to the process current working directory — NOT relative to TaskEnvironment.ProjectDirectory. + /// + [Fact] + public void DeserializePrecomputedCaches_Wave18_8_Disabled_UsesRawPathRelativeToCwd() + { + using TestEnvironment env = TestEnvironment.Create(); + + ChangeWaves.ResetStateForTests(); + env.SetEnvironmentVariable("MSBUILDDISABLEFEATURESFROMVERSION", ChangeWaves.Wave18_8.ToString()); + BuildEnvironmentHelper.ResetInstance_ForUnitTestsOnly(); + try + { + // cacheDir holds the real cache file (and becomes CWD); projectDir is empty. + string cacheDir = env.CreateFolder().Path; + string projectDir = env.CreateFolder().Path; + string cacheFileName = "rar.cache"; + string cacheFullPath = Path.Combine(cacheDir, cacheFileName); + + WriteCacheFileWithSingleEntry(cacheFullPath, "TestAssembly.dll"); + + env.SetCurrentDirectory(cacheDir); + + ITaskItem[] stateFiles = [new TaskItem(cacheFileName)]; + TaskEnvironment taskEnvironment = TaskEnvironment.CreateWithProjectDirectoryAndEnvironment(projectDir); + + SystemState result = SystemState.DeserializePrecomputedCaches( + stateFiles, + null, + _ => true, + taskEnvironment); + + // Under wave-off, the bare relative path is resolved via CWD (= cacheDir), + // where the cache exists, so deserialization succeeded and the entry was processed. + result.ShouldNotBeNull(); + result.instanceLocalFileStateCache.ShouldNotBeEmpty(); + } + finally + { + ChangeWaves.ResetStateForTests(); + } + } + + /// + /// Writes a serialized SystemState cache with a single entry to . + /// + private static void WriteCacheFileWithSingleEntry(string cacheFullPath, string relativeKey) + { + SystemState sysState = new(); + sysState.instanceLocalOutgoingFileStateCache[relativeKey] = new SystemState.FileState(DateTime.UtcNow); + sysState.SerializeCache(cacheFullPath, log: null); + } } } diff --git a/src/Tasks.UnitTests/AssemblyDependency/ResolveAssemblyReferenceTestFixture.cs b/src/Tasks.UnitTests/AssemblyDependency/ResolveAssemblyReferenceTestFixture.cs index b6f6069a1af..2b2c0598aaf 100644 --- a/src/Tasks.UnitTests/AssemblyDependency/ResolveAssemblyReferenceTestFixture.cs +++ b/src/Tasks.UnitTests/AssemblyDependency/ResolveAssemblyReferenceTestFixture.cs @@ -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/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..e295a012fa1 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,20 @@ public override bool Resolve( { foreach (AssemblyFoldersExInfo assemblyFolder in _assemblyFoldersCache.AssemblyFoldersEx) { - string candidatePath = ResolveFromDirectory(assemblyName, isPrimaryProjectReference, wantSpecificVersion, executableExtensions, assemblyFolder.DirectoryPath, assembliesConsideredAndRejected); + // Null is a silent no-op: ResolveFromDirectory short-circuits when fullPathToDirectory is null. + if (assemblyFolder.DirectoryPath is null) + { + continue; + } + + // Pre-MT, an empty registry entry silently resolved to the project directory via + // process CWD. Preserve that behavior by resolving empty entries against the project + // directory via TaskEnvironment. Non-empty registry paths should already be absolute + // but are absolutized defensively. + string directoryPath = assemblyFolder.DirectoryPath.Length == 0 + ? taskEnvironment.ProjectDirectory + : taskEnvironment.GetAbsolutePath(assemblyFolder.DirectoryPath); + string candidatePath = ResolveFromDirectory(assemblyName, isPrimaryProjectReference, wantSpecificVersion, executableExtensions, directoryPath, assembliesConsideredAndRejected); // We have a full path returned if (candidatePath != null) @@ -280,12 +293,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..873e0002b42 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,18 @@ public override bool Resolve( { foreach (AssemblyFoldersFromConfigInfo assemblyFolder in _assemblyFoldersCache.AssemblyFoldersFromConfig) { - string candidatePath = ResolveFromDirectory(assemblyName, isPrimaryProjectReference, wantSpecificVersion, executableExtensions, assemblyFolder.DirectoryPath, assembliesConsideredAndRejected); + // DirectoryPath is guaranteed non-null by the AssemblyFoldersFromConfigInfo ctor's + // VerifyThrowArgumentNull check, so we only need to handle the empty-string case. + // Pre-MT, an empty in the config file silently resolved to the + // project directory via process CWD. Preserve that behavior by explicitly resolving + // empty entries against the project directory via TaskEnvironment. + string directoryPath = assemblyFolder.DirectoryPath.Length == 0 + ? taskEnvironment.ProjectDirectory.Value + // Absolutize via TaskEnvironment: config paths may be relative, and the + // process CWD is no longer guaranteed to be the project directory under MT. + : 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..ccf40f2c799 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,20 @@ public override bool Resolve( // {AssemblyFolders} was passed in. foreach (string assemblyFolder in AssemblyFolder.GetAssemblyFolders(assemblyFolderKey)) { - string resolvedPath = ResolveFromDirectory(assemblyName, isPrimaryProjectReference, wantSpecificVersion, executableExtensions, assemblyFolder, assembliesConsideredAndRejected); + // Null is a silent no-op: ResolveFromDirectory short-circuits when fullPathToDirectory is null. + if (assemblyFolder is null) + { + continue; + } + + // Pre-MT, an empty registry entry silently resolved to the project directory via + // process CWD. Preserve that behavior by resolving empty entries against the project + // directory via TaskEnvironment. + AbsolutePath folderForResolution = assemblyFolder.Length == 0 + ? taskEnvironment.ProjectDirectory + : taskEnvironment.GetAbsolutePath(assemblyFolder); + + string resolvedPath = ResolveFromDirectory(assemblyName, isPrimaryProjectReference, wantSpecificVersion, executableExtensions, folderForResolution, 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 ac677c05ca6..f8fc9b1b328 100644 --- a/src/Tasks/AssemblyDependency/GlobalAssemblyCache.cs +++ b/src/Tasks/AssemblyDependency/GlobalAssemblyCache.cs @@ -275,7 +275,12 @@ internal static string GetLocation( bool specificVersion) { ConcurrentDictionary fusionNameToResolvedPath = null; + // The GAC is a process-wide / machine-wide resource, so the MSBUILDDISABLEGACRARCACHE + // opt-out is read from the process environment rather than the per-task TaskEnvironment. + // Per-task overrides of MSBUILD* environment variables are not supported. +#pragma warning disable MSBuildTask0002 bool useGacRarCache = Environment.GetEnvironmentVariable("MSBUILDDISABLEGACRARCACHE") == null; +#pragma warning restore MSBuildTask0002 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/OutOfProcRarNodeEndpoint.cs b/src/Tasks/AssemblyDependency/Node/OutOfProcRarNodeEndpoint.cs index e6576665003..db5ec41f2dd 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,24 @@ 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."); + + // The TaskEnvironment driver here uses the RAR node process's environment variables + // because the client currently only sends the project directory across the wire. + // When the wire protocol is extended to carry the client's environment variables, + // construct the driver from those values instead so the task sees the same environment the client did. + 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..731f95d5581 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) { } @@ -41,13 +42,20 @@ public override bool Resolve( foundPath = null; userRequestedSpecificFile = false; - if (rawFileNameCandidate != null) + if (rawFileNameCandidate is not null) { - // {RawFileName} was passed in. - if (isImmutableFrameworkReference || fileExists(rawFileNameCandidate)) + // For empty input, keep the empty string as-is to preserve the pre-MT diagnostic: + // fileExists("") returns false and the entry is added to assembliesConsideredAndRejected. + // For non-empty input, absolutize via TaskEnvironment so resolution targets the project + // directory rather than the process CWD (which is no longer per-project under MT). + string fullRawFileName = rawFileNameCandidate.Length == 0 + ? rawFileNameCandidate + : taskEnvironment.GetAbsolutePath(rawFileNameCandidate).Value; + + if (isImmutableFrameworkReference || fileExists(fullRawFileName)) { userRequestedSpecificFile = true; - foundPath = rawFileNameCandidate; + foundPath = fullRawFileName; return true; } @@ -55,7 +63,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..114857f7d35 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. @@ -114,7 +115,8 @@ private static class Strings public static string UnifiedDependency; public static string UnifiedPrimaryReference; - private static bool initialized = false; + private static volatile bool initialized; + private static readonly LockType s_initializeLock = new(); internal static void Initialize(TaskLoggingHelper log) { @@ -123,48 +125,56 @@ internal static void Initialize(TaskLoggingHelper log) return; } - initialized = true; - - string GetResource(string name) => log.GetResourceMessage(name); - string GetResourceFourSpaces(string name) => FourSpaces + log.GetResourceMessage(name); - string GetResourceEightSpaces(string name) => EightSpaces + log.GetResourceMessage(name); - - ConsideredAndRejectedBecauseFusionNamesDidntMatch = GetResourceEightSpaces("ResolveAssemblyReference.ConsideredAndRejectedBecauseFusionNamesDidntMatch"); - ConsideredAndRejectedBecauseNoFile = GetResourceEightSpaces("ResolveAssemblyReference.ConsideredAndRejectedBecauseNoFile"); - ConsideredAndRejectedBecauseNotAFileNameOnDisk = GetResourceEightSpaces("ResolveAssemblyReference.ConsideredAndRejectedBecauseNotAFileNameOnDisk"); - ConsideredAndRejectedBecauseNotInGac = GetResourceEightSpaces("ResolveAssemblyReference.ConsideredAndRejectedBecauseNotInGac"); - ConsideredAndRejectedBecauseTargetDidntHaveFusionName = GetResourceEightSpaces("ResolveAssemblyReference.ConsideredAndRejectedBecauseTargetDidntHaveFusionName"); - Dependency = GetResource("ResolveAssemblyReference.Dependency"); - FormattedAssemblyInfo = GetResourceFourSpaces("ResolveAssemblyReference.FormattedAssemblyInfo"); - FoundRelatedFile = GetResourceFourSpaces("ResolveAssemblyReference.FoundRelatedFile"); - FoundSatelliteFile = GetResourceFourSpaces("ResolveAssemblyReference.FoundSatelliteFile"); - FoundScatterFile = GetResourceFourSpaces("ResolveAssemblyReference.FoundScatterFile"); - ImageRuntimeVersion = GetResourceFourSpaces("ResolveAssemblyReference.ImageRuntimeVersion"); - IsAWinMdFile = GetResourceFourSpaces("ResolveAssemblyReference.IsAWinMdFile"); - LogAttributeFormat = GetResourceEightSpaces("ResolveAssemblyReference.LogAttributeFormat"); - LogTaskPropertyFormat = GetResource("ResolveAssemblyReference.LogTaskPropertyFormat"); - NoBecauseBadImage = GetResourceFourSpaces("ResolveAssemblyReference.NoBecauseBadImage"); - NoBecauseParentReferencesFoundInGac = GetResourceFourSpaces("ResolveAssemblyReference.NoBecauseParentReferencesFoundInGac"); - NotCopyLocalBecauseConflictVictim = GetResourceFourSpaces("ResolveAssemblyReference.NotCopyLocalBecauseConflictVictim"); - NotCopyLocalBecauseEmbedded = GetResourceFourSpaces("ResolveAssemblyReference.NotCopyLocalBecauseEmbedded"); - NotCopyLocalBecauseFrameworksFiles = GetResourceFourSpaces("ResolveAssemblyReference.NotCopyLocalBecauseFrameworksFiles"); - NotCopyLocalBecauseIncomingItemAttributeOverrode = GetResourceFourSpaces("ResolveAssemblyReference.NotCopyLocalBecauseIncomingItemAttributeOverrode"); - NotCopyLocalBecausePrerequisite = GetResourceFourSpaces("ResolveAssemblyReference.NotCopyLocalBecausePrerequisite"); - NotCopyLocalBecauseReferenceFoundInGAC = GetResourceFourSpaces("ResolveAssemblyReference.NotCopyLocalBecauseReferenceFoundInGAC"); - PrimaryReference = GetResource("ResolveAssemblyReference.PrimaryReference"); - RemappedReference = GetResourceFourSpaces("ResolveAssemblyReference.RemappedReference"); - RequiredBy = GetResourceFourSpaces("ResolveAssemblyReference.RequiredBy"); - Resolved = GetResourceFourSpaces("ResolveAssemblyReference.Resolved"); - ResolvedFrom = GetResourceFourSpaces("ResolveAssemblyReference.ResolvedFrom"); - SearchedAssemblyFoldersEx = GetResourceEightSpaces("ResolveAssemblyReference.SearchedAssemblyFoldersEx"); - SearchPath = GetResourceEightSpaces("ResolveAssemblyReference.SearchPath"); - SearchPathAddedByParentAssembly = GetResourceEightSpaces("ResolveAssemblyReference.SearchPathAddedByParentAssembly"); - TargetedProcessorArchitectureDoesNotMatch = GetResourceEightSpaces("ResolveAssemblyReference.TargetedProcessorArchitectureDoesNotMatch"); - UnificationByAppConfig = GetResourceFourSpaces("ResolveAssemblyReference.UnificationByAppConfig"); - UnificationByAutoUnify = GetResourceFourSpaces("ResolveAssemblyReference.UnificationByAutoUnify"); - UnificationByFrameworkRetarget = GetResourceFourSpaces("ResolveAssemblyReference.UnificationByFrameworkRetarget"); - UnifiedDependency = GetResource("ResolveAssemblyReference.UnifiedDependency"); - UnifiedPrimaryReference = GetResource("ResolveAssemblyReference.UnifiedPrimaryReference"); + lock (s_initializeLock) + { + if (initialized) + { + return; + } + + string GetResource(string name) => log.GetResourceMessage(name); + string GetResourceFourSpaces(string name) => FourSpaces + log.GetResourceMessage(name); + string GetResourceEightSpaces(string name) => EightSpaces + log.GetResourceMessage(name); + + ConsideredAndRejectedBecauseFusionNamesDidntMatch = GetResourceEightSpaces("ResolveAssemblyReference.ConsideredAndRejectedBecauseFusionNamesDidntMatch"); + ConsideredAndRejectedBecauseNoFile = GetResourceEightSpaces("ResolveAssemblyReference.ConsideredAndRejectedBecauseNoFile"); + ConsideredAndRejectedBecauseNotAFileNameOnDisk = GetResourceEightSpaces("ResolveAssemblyReference.ConsideredAndRejectedBecauseNotAFileNameOnDisk"); + ConsideredAndRejectedBecauseNotInGac = GetResourceEightSpaces("ResolveAssemblyReference.ConsideredAndRejectedBecauseNotInGac"); + ConsideredAndRejectedBecauseTargetDidntHaveFusionName = GetResourceEightSpaces("ResolveAssemblyReference.ConsideredAndRejectedBecauseTargetDidntHaveFusionName"); + Dependency = GetResource("ResolveAssemblyReference.Dependency"); + FormattedAssemblyInfo = GetResourceFourSpaces("ResolveAssemblyReference.FormattedAssemblyInfo"); + FoundRelatedFile = GetResourceFourSpaces("ResolveAssemblyReference.FoundRelatedFile"); + FoundSatelliteFile = GetResourceFourSpaces("ResolveAssemblyReference.FoundSatelliteFile"); + FoundScatterFile = GetResourceFourSpaces("ResolveAssemblyReference.FoundScatterFile"); + ImageRuntimeVersion = GetResourceFourSpaces("ResolveAssemblyReference.ImageRuntimeVersion"); + IsAWinMdFile = GetResourceFourSpaces("ResolveAssemblyReference.IsAWinMdFile"); + LogAttributeFormat = GetResourceEightSpaces("ResolveAssemblyReference.LogAttributeFormat"); + LogTaskPropertyFormat = GetResource("ResolveAssemblyReference.LogTaskPropertyFormat"); + NoBecauseBadImage = GetResourceFourSpaces("ResolveAssemblyReference.NoBecauseBadImage"); + NoBecauseParentReferencesFoundInGac = GetResourceFourSpaces("ResolveAssemblyReference.NoBecauseParentReferencesFoundInGac"); + NotCopyLocalBecauseConflictVictim = GetResourceFourSpaces("ResolveAssemblyReference.NotCopyLocalBecauseConflictVictim"); + NotCopyLocalBecauseEmbedded = GetResourceFourSpaces("ResolveAssemblyReference.NotCopyLocalBecauseEmbedded"); + NotCopyLocalBecauseFrameworksFiles = GetResourceFourSpaces("ResolveAssemblyReference.NotCopyLocalBecauseFrameworksFiles"); + NotCopyLocalBecauseIncomingItemAttributeOverrode = GetResourceFourSpaces("ResolveAssemblyReference.NotCopyLocalBecauseIncomingItemAttributeOverrode"); + NotCopyLocalBecausePrerequisite = GetResourceFourSpaces("ResolveAssemblyReference.NotCopyLocalBecausePrerequisite"); + NotCopyLocalBecauseReferenceFoundInGAC = GetResourceFourSpaces("ResolveAssemblyReference.NotCopyLocalBecauseReferenceFoundInGAC"); + PrimaryReference = GetResource("ResolveAssemblyReference.PrimaryReference"); + RemappedReference = GetResourceFourSpaces("ResolveAssemblyReference.RemappedReference"); + RequiredBy = GetResourceFourSpaces("ResolveAssemblyReference.RequiredBy"); + Resolved = GetResourceFourSpaces("ResolveAssemblyReference.Resolved"); + ResolvedFrom = GetResourceFourSpaces("ResolveAssemblyReference.ResolvedFrom"); + SearchedAssemblyFoldersEx = GetResourceEightSpaces("ResolveAssemblyReference.SearchedAssemblyFoldersEx"); + SearchPath = GetResourceEightSpaces("ResolveAssemblyReference.SearchPath"); + SearchPathAddedByParentAssembly = GetResourceEightSpaces("ResolveAssemblyReference.SearchPathAddedByParentAssembly"); + TargetedProcessorArchitectureDoesNotMatch = GetResourceEightSpaces("ResolveAssemblyReference.TargetedProcessorArchitectureDoesNotMatch"); + UnificationByAppConfig = GetResourceFourSpaces("ResolveAssemblyReference.UnificationByAppConfig"); + UnificationByAutoUnify = GetResourceFourSpaces("ResolveAssemblyReference.UnificationByAutoUnify"); + UnificationByFrameworkRetarget = GetResourceFourSpaces("ResolveAssemblyReference.UnificationByFrameworkRetarget"); + UnifiedDependency = GetResource("ResolveAssemblyReference.UnifiedDependency"); + UnifiedPrimaryReference = GetResource("ResolveAssemblyReference.UnifiedPrimaryReference"); + + initialized = true; + } } } @@ -179,13 +189,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 +223,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 +304,12 @@ public string[] LatestTargetFrameworkDirectories { get { - return _latestTargetFrameworkDirectories; + return _latestTargetFrameworkDirectories.ToOriginalValueArray(); } set { - _latestTargetFrameworkDirectories = value; + _latestTargetFrameworkDirectories = MakeCanonicalPaths(value); } } @@ -392,15 +404,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.ToOriginalValueArray(); + } + set + { + _candidateAssemblyFiles = MakeAbsolutePaths(value); + } } /// @@ -421,8 +439,14 @@ public ITaskItem[] ResolvedSDKReferences /// public string[] TargetFrameworkDirectories { - get { return _targetFrameworkDirectories; } - set { _targetFrameworkDirectories = value; } + get + { + return _targetFrameworkDirectories.ToOriginalValueArray(); + } + set + { + _targetFrameworkDirectories = MakeCanonicalPaths(value); + } } /// @@ -594,7 +618,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 +707,28 @@ public string[] AllowedRelatedFileExtensions /// public string AppConfigFile { - get { return _appConfigFile; } - set { _appConfigFile = value; } + get => _appConfigFile.OriginalValue; + set + { + // Keep _appConfigFile and _appConfigValueIsEmptyString consistent on every assignment + // so a previous non-empty value isn't left behind when the user reassigns to "" or null. + // + // non-empty -> absolutize into _appConfigFile, clear the empty-string flag. + // empty "" -> clear _appConfigFile; set the empty-string flag only when Wave18_8 is + // disabled, so the legacy "path cannot be empty" error path can fire. + // null -> clear _appConfigFile and the flag. Null means "not set". + if (!string.IsNullOrEmpty(value)) + { + _appConfigFile = MakeAbsolutePath(value); + _appConfigValueIsEmptyString = false; + } + else + { + _appConfigFile = default; + _appConfigValueIsEmptyString = value == string.Empty + && !ChangeWaves.AreFeaturesEnabled(ChangeWaves.Wave18_8); + } + } } /// @@ -764,8 +815,11 @@ public bool DoNotCopyLocalIfInGac /// public string StateFile { - get { return _stateFile; } - set { _stateFile = value; } + get => _stateFile.OriginalValue; + set + { + _stateFile = MakeAbsolutePath(value); + } } /// @@ -909,18 +963,21 @@ public string[] FullFrameworkFolders { get { - return _fullFrameworkFolders; + return _fullFrameworkFolders.ToOriginalValueArray(); } set { ErrorUtilities.VerifyThrowArgumentNull(value, "FullFrameworkFolders"); - _fullFrameworkFolders = value; + _fullFrameworkFolders = MakeCanonicalPaths(value); } } public bool FailIfNotIncremental { get; set; } + /// + public TaskEnvironment TaskEnvironment { get; set; } = TaskEnvironment.Fallback; + /// /// Allow the task to run on the out-of-proc node if enabled for this build. /// @@ -1088,6 +1145,77 @@ 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) + { + if (paths is null) + { + return []; + } + AbsolutePath[] result = new AbsolutePath[paths.Length]; + for (int i = 0; i < paths.Length; i++) + { + result[i] = MakeAbsolutePath(paths[i]); + } + return result; + } + + /// + /// 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).GetCanonicalFormNoThrow(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) + { + if (paths is null) + { + return []; + } + AbsolutePath[] result = new AbsolutePath[paths.Length]; + for (int i = 0; i < paths.Length; i++) + { + result[i] = MakeCanonicalPath(paths[i]); + } + return result; + } #endregion #region Logging @@ -1533,11 +1661,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 +1711,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 +1760,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 +1835,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 +2235,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 +2249,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 +2268,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 +2365,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 +2396,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 +2539,26 @@ 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 + // and the ResolveAssemblyReference.AppConfigFilePathEmpty resource. + Log.LogErrorWithCodeFromResources( + "ResolveAssemblyReference.InvalidAppConfig", + string.Empty, + ResourceUtilities.GetResourceString("ResolveAssemblyReference.AppConfigFilePathEmpty")); + 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 +2581,9 @@ internal bool Execute( _searchPaths, _allowedAssemblyExtensions, _relatedFileExtensions, - _candidateAssemblyFiles, + _candidateAssemblyFiles.ToStringArray(), _resolvedSDKReferences, - _targetFrameworkDirectories, + _targetFrameworkDirectories.ToStringArray(), installedAssemblies, processorArchitecture, fileExists, @@ -2457,7 +2601,7 @@ internal bool Execute( _projectTargetFramework, frameworkMoniker, Log, - _latestTargetFrameworkDirectories, + _latestTargetFrameworkDirectories.ToStringArray(), _copyLocalDependenciesWhenParentReferenceInGac, DoNotCopyLocalIfInGac, getAssemblyPathInGac, @@ -2468,7 +2612,8 @@ internal bool Execute( _ignoreTargetFrameworkAttributeVersionMismatch, _unresolveFrameworkAssembliesFromHigherFrameworks, assemblyMetadataCache, - _nonCultureResourceDirectories); + _nonCultureResourceDirectories, + TaskEnvironment); dependencyTable.FindDependenciesOfExternallyResolvedReferences = FindDependenciesOfExternallyResolvedReferences; @@ -2634,9 +2779,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 +2967,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.ToStringArray()); 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 +3056,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 +3087,7 @@ private void DumpTargetProfileLists(AssemblyTableInfo[] installedAssemblyTableIn return; } - string dumpFrameworkSubsetList = Environment.GetEnvironmentVariable("MSBUILDDUMPFRAMEWORKSUBSETLIST"); + string dumpFrameworkSubsetList = TaskEnvironment.GetEnvironmentVariable("MSBUILDDUMPFRAMEWORKSUBSETLIST"); if (dumpFrameworkSubsetList == null) { return; @@ -2955,7 +3100,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 +3111,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 +3229,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 +3253,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]; @@ -3265,6 +3410,18 @@ public override bool Execute() && BuildEngine is IBuildEngine10 buildEngine10 && buildEngine10.EngineServices.IsOutOfProcRarNodeEnabled) { + // RAR-as-a-service is not yet safe to use under multithreaded mode of execution. + // The shared OutOfProcRarClient instance (per build, per node) holds a single pipe + // connection that cannot be used concurrently from multiple RAR tasks running on + // separate threads. Pipe pooling and TOCTOU safety for the shared client are tracked + // as a follow-up; until that lands, fail fast rather than corrupt the build. + if (TaskEnvironment.IsMultiThreaded) + { + throw new NotSupportedException( + "ResolveAssemblyReference does not currently support running with both " + + "out-of-proc RAR node and multithreaded mode enabled at the same time."); + } + try { #pragma warning disable CA2000 // The OutOfProcRarClient is disposable but its disposal is handled by RegisterTaskObject. @@ -3274,9 +3431,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..99ee906f883 100644 --- a/src/Tasks/GetReferenceAssemblyPaths.cs +++ b/src/Tasks/GetReferenceAssemblyPaths.cs @@ -307,4 +307,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..fbeacd22c87 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)).GetCanonicalFormNoThrow(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..9058e59073e 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 normalize path "{0}" by resolving "." and ".." segments: {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: "} @@ -1529,6 +1532,10 @@ MSB3249: Application Configuration file "{0}" is invalid. {1} {StrBegin="MSB3249: "} + + Application Configuration file path cannot be empty. + This message can be used as the {1} in MSB3249 + MSB3250: The file "{0}" will be ignored because it cannot be read. This file was either passed in to InstalledAssemblyTables or was found by searching the {1} folder in the TargetFrameworkDirectories. {2} {StrBegin="MSB3250: "} diff --git a/src/Tasks/Resources/xlf/Strings.cs.xlf b/src/Tasks/Resources/xlf/Strings.cs.xlf index dde273ca0ae..641b7a045dc 100644 --- a/src/Tasks/Resources/xlf/Strings.cs.xlf +++ b/src/Tasks/Resources/xlf/Strings.cs.xlf @@ -1,4 +1,4 @@ - + @@ -619,6 +619,11 @@ Očekávaný soubor {0} neexistuje. + + Could not normalize path "{0}" by resolving "." and ".." segments: {1}. The path will be used as-is. + Could not normalize path "{0}" by resolving "." and ".." segments: {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}. @@ -1983,7 +1988,11 @@ MSB3249: Konfigurační soubor aplikace {0} je neplatný. {1} {StrBegin="MSB3249: "} - + + Application Configuration file path cannot be empty. + Application Configuration file path cannot be empty. + This message can be used as the {1} in MSB3249 + MSB3250: The file "{0}" will be ignored because it cannot be read. This file was either passed in to InstalledAssemblyTables or was found by searching the {1} folder in the TargetFrameworkDirectories. {2} MSB3250: Soubor {0} nelze přečíst a bude ignorován. Tento soubor byl předán parametru InstalledAssemblyTables nebo byl nalezen ve složce {1} určené parametrem TargetFrameworkDirectories. {2} {StrBegin="MSB3250: "} diff --git a/src/Tasks/Resources/xlf/Strings.de.xlf b/src/Tasks/Resources/xlf/Strings.de.xlf index 99732a18ae6..b8fab0f6fa9 100644 --- a/src/Tasks/Resources/xlf/Strings.de.xlf +++ b/src/Tasks/Resources/xlf/Strings.de.xlf @@ -1,4 +1,4 @@ - + @@ -619,6 +619,11 @@ Die erwartete Datei "{0}" ist nicht vorhanden. + + Could not normalize path "{0}" by resolving "." and ".." segments: {1}. The path will be used as-is. + Could not normalize path "{0}" by resolving "." and ".." segments: {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}. @@ -1983,7 +1988,11 @@ MSB3249: Die Anwendungskonfigurationsdatei "{0}" ist ungültig. {1} {StrBegin="MSB3249: "} - + + Application Configuration file path cannot be empty. + Application Configuration file path cannot be empty. + This message can be used as the {1} in MSB3249 + MSB3250: The file "{0}" will be ignored because it cannot be read. This file was either passed in to InstalledAssemblyTables or was found by searching the {1} folder in the TargetFrameworkDirectories. {2} MSB3250: Die Datei "{0}" kann nicht gelesen werden und wird daher ignoriert. Diese Datei wurde an InstalledAssemblyTables übergeben oder beim Durchsuchen des Ordners "{1}" in den TargetFrameworkDirectories gefunden. {2} {StrBegin="MSB3250: "} diff --git a/src/Tasks/Resources/xlf/Strings.es.xlf b/src/Tasks/Resources/xlf/Strings.es.xlf index 2ba711a7b04..396d3d1ce66 100644 --- a/src/Tasks/Resources/xlf/Strings.es.xlf +++ b/src/Tasks/Resources/xlf/Strings.es.xlf @@ -1,4 +1,4 @@ - + @@ -619,6 +619,11 @@ El archivo esperado "{0}" no existe. + + Could not normalize path "{0}" by resolving "." and ".." segments: {1}. The path will be used as-is. + Could not normalize path "{0}" by resolving "." and ".." segments: {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}. @@ -1983,7 +1988,11 @@ MSB3249: El archivo de configuración de la aplicación "{0}" no es válido. {1} {StrBegin="MSB3249: "} - + + Application Configuration file path cannot be empty. + Application Configuration file path cannot be empty. + This message can be used as the {1} in MSB3249 + MSB3250: The file "{0}" will be ignored because it cannot be read. This file was either passed in to InstalledAssemblyTables or was found by searching the {1} folder in the TargetFrameworkDirectories. {2} MSB3250: Se omitirá el archivo "{0}" porque no se puede leer. Bien se pasó a InstalledAssemblyTables o bien se encontró al buscar en la carpeta {1} de TargetFrameworkDirectories. {2} {StrBegin="MSB3250: "} diff --git a/src/Tasks/Resources/xlf/Strings.fr.xlf b/src/Tasks/Resources/xlf/Strings.fr.xlf index a1bbc4343c5..69e6c5dc410 100644 --- a/src/Tasks/Resources/xlf/Strings.fr.xlf +++ b/src/Tasks/Resources/xlf/Strings.fr.xlf @@ -1,4 +1,4 @@ - + @@ -619,6 +619,11 @@ Le fichier attendu "{0}" n'existe pas. + + Could not normalize path "{0}" by resolving "." and ".." segments: {1}. The path will be used as-is. + Could not normalize path "{0}" by resolving "." and ".." segments: {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}. @@ -1983,7 +1988,11 @@ MSB3249: Le fichier de configuration de l'application "{0}" n'est pas valide. {1} {StrBegin="MSB3249: "} - + + Application Configuration file path cannot be empty. + Application Configuration file path cannot be empty. + This message can be used as the {1} in MSB3249 + MSB3250: The file "{0}" will be ignored because it cannot be read. This file was either passed in to InstalledAssemblyTables or was found by searching the {1} folder in the TargetFrameworkDirectories. {2} MSB3250: Le fichier "{0}" va être ignoré, car il ne peut pas être lu. Ce fichier a été passé dans InstalledAssemblyTables ou a été trouvé lors de la recherche du dossier {1} dans TargetFrameworkDirectories. {2} {StrBegin="MSB3250: "} diff --git a/src/Tasks/Resources/xlf/Strings.it.xlf b/src/Tasks/Resources/xlf/Strings.it.xlf index b5aba491c4a..70ced74ba52 100644 --- a/src/Tasks/Resources/xlf/Strings.it.xlf +++ b/src/Tasks/Resources/xlf/Strings.it.xlf @@ -1,4 +1,4 @@ - + @@ -619,6 +619,11 @@ Il file previsto "{0}" non esiste. + + Could not normalize path "{0}" by resolving "." and ".." segments: {1}. The path will be used as-is. + Could not normalize path "{0}" by resolving "." and ".." segments: {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}. @@ -1983,7 +1988,11 @@ MSB3249: file di configurazione dell'applicazione "{0}" non valido. {1} {StrBegin="MSB3249: "} - + + Application Configuration file path cannot be empty. + Application Configuration file path cannot be empty. + This message can be used as the {1} in MSB3249 + MSB3250: The file "{0}" will be ignored because it cannot be read. This file was either passed in to InstalledAssemblyTables or was found by searching the {1} folder in the TargetFrameworkDirectories. {2} MSB3250: il file "{0}" verrà ignorato perché non può essere letto. Il file è stato passato a InstalledAssemblyTables o è stato trovato cercando nella cartella {1} in TargetFrameworkDirectories. {2} {StrBegin="MSB3250: "} diff --git a/src/Tasks/Resources/xlf/Strings.ja.xlf b/src/Tasks/Resources/xlf/Strings.ja.xlf index 1c9a490b7e7..535a63d5e98 100644 --- a/src/Tasks/Resources/xlf/Strings.ja.xlf +++ b/src/Tasks/Resources/xlf/Strings.ja.xlf @@ -1,4 +1,4 @@ - + @@ -619,6 +619,11 @@ 指定されたファイル "{0}" は存在しません。 + + Could not normalize path "{0}" by resolving "." and ".." segments: {1}. The path will be used as-is. + Could not normalize path "{0}" by resolving "." and ".." segments: {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} をインストールしてください。 @@ -1983,7 +1988,11 @@ MSB3249: アプリケーション構成ファイル "{0}" は無効です。{1} {StrBegin="MSB3249: "} - + + Application Configuration file path cannot be empty. + Application Configuration file path cannot be empty. + This message can be used as the {1} in MSB3249 + MSB3250: The file "{0}" will be ignored because it cannot be read. This file was either passed in to InstalledAssemblyTables or was found by searching the {1} folder in the TargetFrameworkDirectories. {2} MSB3250: ファイル "{0}" は、読み取れないため無視されます。このファイルは、InstalledAssemblyTables に渡されたか、TargetFrameworkDirectories の {1} フォルダーの検索によって見つかりました。{2} {StrBegin="MSB3250: "} diff --git a/src/Tasks/Resources/xlf/Strings.ko.xlf b/src/Tasks/Resources/xlf/Strings.ko.xlf index 5a2d828cb01..dba820eb07e 100644 --- a/src/Tasks/Resources/xlf/Strings.ko.xlf +++ b/src/Tasks/Resources/xlf/Strings.ko.xlf @@ -1,4 +1,4 @@ - + @@ -619,6 +619,11 @@ 필요한 "{0}" 파일이 없습니다. + + Could not normalize path "{0}" by resolving "." and ".." segments: {1}. The path will be used as-is. + Could not normalize path "{0}" by resolving "." and ".." segments: {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}을(를) 설치하세요. @@ -1983,7 +1988,11 @@ MSB3249: 애플리케이션 구성 파일 "{0}"이(가) 잘못되었습니다. {1} {StrBegin="MSB3249: "} - + + Application Configuration file path cannot be empty. + Application Configuration file path cannot be empty. + This message can be used as the {1} in MSB3249 + MSB3250: The file "{0}" will be ignored because it cannot be read. This file was either passed in to InstalledAssemblyTables or was found by searching the {1} folder in the TargetFrameworkDirectories. {2} MSB3250: "{0}" 파일은 읽어올 수 없으므로 무시됩니다. 이 파일을 InstalledAssemblyTables에 전달했거나 TargetFrameworkDirectories에서 {1} 폴더를 검색하여 찾았습니다. {2} {StrBegin="MSB3250: "} diff --git a/src/Tasks/Resources/xlf/Strings.pl.xlf b/src/Tasks/Resources/xlf/Strings.pl.xlf index 8e8b2953fcb..3b950f4f12b 100644 --- a/src/Tasks/Resources/xlf/Strings.pl.xlf +++ b/src/Tasks/Resources/xlf/Strings.pl.xlf @@ -1,4 +1,4 @@ - + @@ -619,6 +619,11 @@ Oczekiwany plik „{0}” nie istnieje. + + Could not normalize path "{0}" by resolving "." and ".." segments: {1}. The path will be used as-is. + Could not normalize path "{0}" by resolving "." and ".." segments: {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}. @@ -1983,7 +1988,11 @@ MSB3249: Plik konfiguracyjny aplikacji „{0}” jest nieprawidłowy. {1} {StrBegin="MSB3249: "} - + + Application Configuration file path cannot be empty. + Application Configuration file path cannot be empty. + This message can be used as the {1} in MSB3249 + MSB3250: The file "{0}" will be ignored because it cannot be read. This file was either passed in to InstalledAssemblyTables or was found by searching the {1} folder in the TargetFrameworkDirectories. {2} MSB3250: Plik „{0}” zostanie zignorowany, ponieważ nie można go odczytać. Plik ten został przekazany do parametru InstalledAssemblyTables lub znaleziony podczas przeszukiwania folderu {1} określonego w parametrze TargetFrameworkDirectories. {2} {StrBegin="MSB3250: "} diff --git a/src/Tasks/Resources/xlf/Strings.pt-BR.xlf b/src/Tasks/Resources/xlf/Strings.pt-BR.xlf index bdbc7054eee..4626c0cb203 100644 --- a/src/Tasks/Resources/xlf/Strings.pt-BR.xlf +++ b/src/Tasks/Resources/xlf/Strings.pt-BR.xlf @@ -1,4 +1,4 @@ - + @@ -619,6 +619,11 @@ O arquivo esperado "{0}" não existe. + + Could not normalize path "{0}" by resolving "." and ".." segments: {1}. The path will be used as-is. + Could not normalize path "{0}" by resolving "." and ".." segments: {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}. @@ -1983,7 +1988,11 @@ MSB3249: O arquivo de Configuração de Aplicativo "{0}" é inválido. {1} {StrBegin="MSB3249: "} - + + Application Configuration file path cannot be empty. + Application Configuration file path cannot be empty. + This message can be used as the {1} in MSB3249 + MSB3250: The file "{0}" will be ignored because it cannot be read. This file was either passed in to InstalledAssemblyTables or was found by searching the {1} folder in the TargetFrameworkDirectories. {2} MSB3250: O arquivo "{0}" será ignorado, pois não pode ser lido. Esse arquivo foi passado para o InstalledAssemblyTables ou foi encontrado com a pesquisa da pasta {1} nos TargetFrameworkDirectories. {2} {StrBegin="MSB3250: "} diff --git a/src/Tasks/Resources/xlf/Strings.ru.xlf b/src/Tasks/Resources/xlf/Strings.ru.xlf index f5fedc7bcbe..0bc67650363 100644 --- a/src/Tasks/Resources/xlf/Strings.ru.xlf +++ b/src/Tasks/Resources/xlf/Strings.ru.xlf @@ -1,4 +1,4 @@ - + @@ -619,6 +619,11 @@ Ожидаемый файл "{0}" не существует. + + Could not normalize path "{0}" by resolving "." and ".." segments: {1}. The path will be used as-is. + Could not normalize path "{0}" by resolving "." and ".." segments: {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}. @@ -1983,7 +1988,11 @@ MSB3249: Недопустимый конфигурационный файл приложения "{0}". {1} {StrBegin="MSB3249: "} - + + Application Configuration file path cannot be empty. + Application Configuration file path cannot be empty. + This message can be used as the {1} in MSB3249 + MSB3250: The file "{0}" will be ignored because it cannot be read. This file was either passed in to InstalledAssemblyTables or was found by searching the {1} folder in the TargetFrameworkDirectories. {2} MSB3250: файл "{0}" будет пропущен, так как его невозможно прочитать. Этот файл был передан в InstalledAssemblyTables или обнаружен при поиске в папке "{1}" в TargetFrameworkDirectories. {2} {StrBegin="MSB3250: "} diff --git a/src/Tasks/Resources/xlf/Strings.tr.xlf b/src/Tasks/Resources/xlf/Strings.tr.xlf index 88671129bf6..5550a0c902a 100644 --- a/src/Tasks/Resources/xlf/Strings.tr.xlf +++ b/src/Tasks/Resources/xlf/Strings.tr.xlf @@ -1,4 +1,4 @@ - + @@ -619,6 +619,11 @@ Beklenen "{0}" dosyası yok. + + Could not normalize path "{0}" by resolving "." and ".." segments: {1}. The path will be used as-is. + Could not normalize path "{0}" by resolving "." and ".." segments: {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. @@ -1983,7 +1988,11 @@ MSB3249: "{0}" Uygulama Yapılandırması dosyası geçersiz. {1} {StrBegin="MSB3249: "} - + + Application Configuration file path cannot be empty. + Application Configuration file path cannot be empty. + This message can be used as the {1} in MSB3249 + MSB3250: The file "{0}" will be ignored because it cannot be read. This file was either passed in to InstalledAssemblyTables or was found by searching the {1} folder in the TargetFrameworkDirectories. {2} MSB3250: "{0}" dosyası okunamadığı için yoksayılacak. Bu dosya InstalledAssemblyTables’a geçirildi ya da TargetFrameworkDirectories içinde {1} klasörü aranarak bulundu. {2} {StrBegin="MSB3250: "} diff --git a/src/Tasks/Resources/xlf/Strings.xlf b/src/Tasks/Resources/xlf/Strings.xlf index d3e5dbba7d6..c6063446234 100644 --- a/src/Tasks/Resources/xlf/Strings.xlf +++ b/src/Tasks/Resources/xlf/Strings.xlf @@ -1261,6 +1261,10 @@ MSB3249: Application Configuration file "{0}" is invalid. {1} {StrBegin="MSB3249: "} + + Application Configuration file path cannot be empty. + This message can be used as the {1} in MSB3249 + MSB3250: The file "{0}" will be ignored because it cannot be read. This file was either passed in to InstalledAssemblyTables or was found by searching the {1} folder in the TargetFrameworkDirectories. {2} {StrBegin="MSB3250: "} diff --git a/src/Tasks/Resources/xlf/Strings.zh-Hans.xlf b/src/Tasks/Resources/xlf/Strings.zh-Hans.xlf index 85c481533a7..68d79cfa21e 100644 --- a/src/Tasks/Resources/xlf/Strings.zh-Hans.xlf +++ b/src/Tasks/Resources/xlf/Strings.zh-Hans.xlf @@ -1,4 +1,4 @@ - + @@ -619,6 +619,11 @@ 所需文件“{0}”不存在。 + + Could not normalize path "{0}" by resolving "." and ".." segments: {1}. The path will be used as-is. + Could not normalize path "{0}" by resolving "." and ".." segments: {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}。 @@ -1983,7 +1988,11 @@ MSB3249: 应用程序配置文件“{0}”无效。{1} {StrBegin="MSB3249: "} - + + Application Configuration file path cannot be empty. + Application Configuration file path cannot be empty. + This message can be used as the {1} in MSB3249 + MSB3250: The file "{0}" will be ignored because it cannot be read. This file was either passed in to InstalledAssemblyTables or was found by searching the {1} folder in the TargetFrameworkDirectories. {2} MSB3250: 将忽略文件“{0}”,因为无法读取该文件。此文件是传入到 InstalledAssemblyTables 的,或者是通过在 TargetFrameworkDirectories 中搜索 {1} 文件夹找到的。{2} {StrBegin="MSB3250: "} diff --git a/src/Tasks/Resources/xlf/Strings.zh-Hant.xlf b/src/Tasks/Resources/xlf/Strings.zh-Hant.xlf index f5241dfc562..88da3b30f3e 100644 --- a/src/Tasks/Resources/xlf/Strings.zh-Hant.xlf +++ b/src/Tasks/Resources/xlf/Strings.zh-Hant.xlf @@ -1,4 +1,4 @@ - + @@ -619,6 +619,11 @@ 預期的檔案 "{0}" 不存在。 + + Could not normalize path "{0}" by resolving "." and ".." segments: {1}. The path will be used as-is. + Could not normalize path "{0}" by resolving "." and ".." segments: {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}。 @@ -1983,7 +1988,11 @@ MSB3249: 應用程式組態檔 "{0}" 無效。{1} {StrBegin="MSB3249: "} - + + Application Configuration file path cannot be empty. + Application Configuration file path cannot be empty. + This message can be used as the {1} in MSB3249 + MSB3250: The file "{0}" will be ignored because it cannot be read. This file was either passed in to InstalledAssemblyTables or was found by searching the {1} folder in the TargetFrameworkDirectories. {2} MSB3250: 將忽略檔案 "{0}",因為無法讀取。這個檔案可能傳入 InstalledAssemblyTables,或是在 TargetFrameworkDirectories 中搜尋 {1} 資料夾找到。{2} {StrBegin="MSB3250: "} 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..ae448b6a8a9 100644 --- a/src/Tasks/SystemState.cs +++ b/src/Tasks/SystemState.cs @@ -147,6 +147,11 @@ internal sealed class FileState : ITranslatable /// internal string runtimeVersion; + /// + /// Per-instance lock that serializes concurrent lazy initialization and read of the fields. + /// + internal readonly LockType _lock = new(); + /// /// Default construct. /// @@ -472,29 +477,35 @@ private AssemblyNameExtension GetAssemblyName(string path) // Not a well-known FX assembly so now check the cache. FileState fileState = GetFileState(path); - if (fileState.Assembly == null) - { - fileState.Assembly = getAssemblyName(path); - // Certain assemblies, like mscorlib may not have metadata. - // Avoid continuously calling getAssemblyName on these files by - // recording these as having an empty name. + // Concurrent RAR tasks can share the same FileState reference via s_processWideFileStateCache. + // Lock to safely publish writes to the shared FileState.Assembly field. + lock (fileState._lock) + { if (fileState.Assembly == null) { - fileState.Assembly = AssemblyNameExtension.UnnamedAssembly; + fileState.Assembly = getAssemblyName(path); + + // Certain assemblies, like mscorlib may not have metadata. + // Avoid continuously calling getAssemblyName on these files by + // recording these as having an empty name. + if (fileState.Assembly == null) + { + fileState.Assembly = AssemblyNameExtension.UnnamedAssembly; + } + if (fileState.IsWorthPersisting) + { + isDirty = true; + } } - if (fileState.IsWorthPersisting) + + if (fileState.Assembly.IsUnnamedAssembly) { - isDirty = true; + return null; } - } - if (fileState.Assembly.IsUnnamedAssembly) - { - return null; + return fileState.Assembly; } - - return fileState.Assembly; } /// @@ -504,16 +515,20 @@ private AssemblyNameExtension GetAssemblyName(string path) private string GetRuntimeVersion(string path) { FileState fileState = GetFileState(path); - if (String.IsNullOrEmpty(fileState.RuntimeVersion)) + // Lock to serialize concurrent populate-and-read of the shared RuntimeVersion field. + lock (fileState._lock) { - fileState.RuntimeVersion = getAssemblyRuntimeVersion(path); - if (fileState.IsWorthPersisting) + if (String.IsNullOrEmpty(fileState.RuntimeVersion)) { - isDirty = true; + fileState.RuntimeVersion = getAssemblyRuntimeVersion(path); + if (fileState.IsWorthPersisting) + { + isDirty = true; + } } - } - return fileState.RuntimeVersion; + return fileState.RuntimeVersion; + } } /// @@ -533,34 +548,39 @@ private void GetAssemblyMetadata( out FrameworkName frameworkName) { FileState fileState = GetFileState(path); - if (fileState.dependencies == null) + // Lock to atomically populate-and-read the three metadata fields. + lock (fileState._lock) { - getAssemblyMetadata( - path, - assemblyMetadataCache, - out fileState.dependencies, - out fileState.scatterFiles, - out fileState.frameworkName); - - if (fileState.IsWorthPersisting) + if (fileState.dependencies == null) { - isDirty = true; + getAssemblyMetadata( + path, + assemblyMetadataCache, + out fileState.dependencies, + out fileState.scatterFiles, + out fileState.frameworkName); + + if (fileState.IsWorthPersisting) + { + isDirty = true; + } } - } - dependencies = fileState.dependencies; - scatterFiles = fileState.scatterFiles; - frameworkName = fileState.frameworkName; + dependencies = fileState.dependencies; + scatterFiles = fileState.scatterFiles; + frameworkName = fileState.frameworkName; + } } /// /// 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,19 +588,39 @@ 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; } + + string stateFileDirectory = Path.GetDirectoryName(stateFilePath); foreach (KeyValuePair kvp in sysState.instanceLocalFileStateCache) { string relativePath = kvp.Key; if (!assembliesFound.Contains(relativePath)) { FileState fileState = kvp.Value; - string fullPath = Path.GetFullPath(Path.Combine(Path.GetDirectoryName(stateFile.ToString()), relativePath)); + AbsolutePath fullPath = taskEnvironment.GetAbsolutePath(Path.Combine(stateFileDirectory, relativePath)).GetCanonicalForm(); + if (fileExists(fullPath)) { // Correct file path @@ -599,7 +639,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 +650,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..1e1600549ea 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 { + /// + /// Returns the canonical form of an (resolving ".." segments, etc.), + /// or the original absolute path if canonicalization fails. + /// 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. + /// + /// The absolute path to canonicalize. + /// Optional logger. When provided, a low-importance diagnostic message is logged on failure. + internal static AbsolutePath GetCanonicalFormNoThrow(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 . @@ -34,7 +58,7 @@ internal static class TaskEnvironmentExtensions } /// - /// Converts an array of to a string array. + /// Converts an array of to a string array of s. /// Returns if is . /// internal static string[]? ToStringArray(this AbsolutePath[]? paths) @@ -52,5 +76,26 @@ internal static class TaskEnvironmentExtensions return result; } + + /// + /// Converts an array of to a string array of s + /// (the user-supplied path before any absolutization/canonicalization). + /// Returns if is . + /// + internal static string[]? ToOriginalValueArray(this AbsolutePath[]? paths) + { + if (paths is null) + { + return null; + } + + var result = new string[paths.Length]; + for (int i = 0; i < paths.Length; i++) + { + result[i] = paths[i].OriginalValue; + } + + return result; + } } }