Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
b743044
Make RAR as a service use TaskEnvironment. Make OutOfProcRarClient th…
AR-May Mar 12, 2026
f5caaff
Absolutize RAR task inputs. ReferenceTable should use the task enviro…
AR-May Mar 12, 2026
f090cef
Make resolvers use task environemnt.
AR-May Mar 12, 2026
84ed710
Enlighten GetReferenceAssemblyPaths
AR-May Mar 12, 2026
fdd2f52
Fix tests.
AR-May Mar 12, 2026
b669d97
Set default driver to enlightened tasks.
AR-May Mar 13, 2026
c818294
Merge branch 'main' into enlighten-RAR-2
AR-May Apr 15, 2026
6e770db
Fix bug with sdk paths comparer.
AR-May Apr 15, 2026
161d875
Added AbsolutePath overload for SerializeCache.
AR-May Apr 15, 2026
a7da484
Ensure that resolvers absolutize the inputs. Do not do canonicalizati…
AR-May Apr 15, 2026
f6f5292
Disambiguated the cref to SerializeCache(string, TaskLoggingHelper, b…
AR-May Apr 15, 2026
92f997c
Be defensive and absolutize in AssemblyFoldersFromConfig cache and re…
AR-May Apr 15, 2026
c4653dd
Add comment.
AR-May Apr 15, 2026
77ea94b
defensively assign empty arrays as values if null value is passed.
AR-May Apr 16, 2026
0688f80
Removed the redundant _taskEnvironment field
AR-May Apr 16, 2026
5186ee4
Add tests for the AppConfigFile in RAR
AR-May Apr 16, 2026
347d260
Fix RAR test fixture FileExists.
AR-May Apr 16, 2026
1046cbc
Address null-safety concern.
AR-May Apr 16, 2026
26636a1
Deduplicate code in StateFileBase.
AR-May Apr 16, 2026
04583ae
Address review feedback for tests.
AR-May Apr 16, 2026
289e6da
Place inputs absolutization under a changewave. Canonicalize the path…
AR-May Apr 23, 2026
6979b95
Ensure that canonicalization in AssemblyTableInfo does not throw.
AR-May Apr 23, 2026
74ad0ec
Remove unnecessary GetCanonicalForm in SystemState.cs
AR-May Apr 23, 2026
47bb660
Remove path canonization and thus unnecessary cache in the InstalledS…
AR-May Apr 23, 2026
e246639
Refactor IsFrameworkFile functions.
AR-May Apr 23, 2026
5a9d94e
Defensively absolutize paths from registry.
AR-May Apr 23, 2026
627af67
Refactor the helper methods in RAR task
AR-May Apr 23, 2026
b8295a5
changewave number
JanProvaznik May 4, 2026
afd952d
Remove unnecessary TaskEnvironment = TaskEnvironmentHelper.CreateForT…
JanProvaznik May 4, 2026
80bec33
Merge origin/main and resolve conflicts
JanProvaznik May 4, 2026
4d72a84
Fix stale Wave 18.6 comments to Wave 18.8
JanProvaznik May 4, 2026
797c567
Add ChangeWaves documentation and add more tests.
AR-May May 6, 2026
389a621
Make the canonicalization error message more clear.
AR-May May 6, 2026
95346fb
Minor refactoring
AR-May May 11, 2026
27de13c
Refactor AbsolutePath array conversions.
AR-May May 11, 2026
06bafbf
Localize temporary message.
AR-May May 11, 2026
df7544d
GAC should not work through the TaskEnvironment.
AR-May May 11, 2026
ec60559
Fix empty paths handling in resolvers.
AR-May May 11, 2026
8db2cd0
Fix race condition in Strings.Initialize
AR-May May 11, 2026
e814358
Revert fixes for OOP RAR client support in MT mode, and add a clear e…
AR-May May 11, 2026
4db964f
Remove redundant TaskEnvironmentHelper.CreateForTest()
AR-May May 11, 2026
cf673f8
Address pr comments: minor refactorings.
AR-May May 12, 2026
1fa6f6d
Use public API for setting default TaskEnvironment.
AR-May May 13, 2026
2f8ee2c
Fix race condition in SystemState.
AR-May May 13, 2026
cc8e522
Merge branch 'main' into enlighten-RAR-2
AR-May May 13, 2026
1787f42
Drop wave gate for empty-path handling in RAR resolvers. Remove tests.
AR-May May 18, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions documentation/wiki/ChangeWaves.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
3 changes: 2 additions & 1 deletion src/Framework/ChangeWaves.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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];
Comment thread
AR-May marked this conversation as resolved.

/// <summary>
/// Special value indicating that all features behind all Change Waves should be enabled.
Expand Down
7 changes: 7 additions & 0 deletions src/Framework/TaskEnvironment.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,13 @@ internal TaskEnvironment(ITaskEnvironmentDriver driver)
/// </remarks>
public static TaskEnvironment Fallback { get; } = new(MultiProcessTaskEnvironmentDriver.Instance);

/// <summary>
/// Gets a value indicating whether this <see cref="TaskEnvironment"/> is providing
/// per-task isolated state (multithreaded mode). When <see langword="false"/>, the
/// environment delegates to the shared process environment (multi-process mode).
/// </summary>
internal bool IsMultiThreaded => _driver is MultiThreadedTaskEnvironmentDriver;

/// <summary>
/// Creates a new <see cref="TaskEnvironment"/> with isolated working directory and environment variables.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand Down
53 changes: 43 additions & 10 deletions src/Tasks.UnitTests/AssemblyDependency/Miscellaneous.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1135,23 +1135,56 @@ public void Regress286699_InvalidSearchPath()
}

/// <summary>
/// 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.
/// </summary>
[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.
/// <summary>
/// When Wave18_8 is disabled, empty AppConfigFile should cause the task to fail
/// with an error, preserving backward-compatible behavior.
/// </summary>
[Fact]
public void EmptyAppConfigFile_Wave18_8_Disabled_Fails()
Comment thread
AR-May marked this conversation as resolved.
{
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();
}
}

/// <summary>
Expand Down Expand Up @@ -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<string>());
null, null, null, new Version("4.0"), null, null, null, true, false, null, null, false, null, WarnOrErrorOnTargetArchitectureMismatchBehavior.None, false, false, null, Array.Empty<string>(), TaskEnvironmentHelper.CreateForTest());
MockEngine mockEngine;
ResolveAssemblyReference rar;
Dictionary<string, string> denyList;
Expand Down Expand Up @@ -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<string>());
null, null, new Version("4.0"), null, log, null, true, false, null, null, false, null, WarnOrErrorOnTargetArchitectureMismatchBehavior.None, false, false, null, Array.Empty<string>(), TaskEnvironmentHelper.CreateForTest());
return referenceTable;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}

/// <summary>
/// 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.
/// </summary>
[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();
}

/// <summary>
/// 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.
/// </summary>
[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();
}
}

/// <summary>
/// Writes a serialized SystemState cache with a single entry to <paramref name="cacheFullPath"/>.
/// </summary>
private static void WriteCacheFileWithSingleEntry(string cacheFullPath, string relativeKey)
{
SystemState sysState = new();
sysState.instanceLocalOutgoingFileStateCache[relativeKey] = new SystemState.FileState(DateTime.UtcNow);
sysState.SerializeCache(cacheFullPath, log: null);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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)
{
Expand Down Expand Up @@ -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
(
Expand Down
5 changes: 3 additions & 2 deletions src/Tasks.UnitTests/HintPathResolver_Tests.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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: "",
Expand Down
8 changes: 6 additions & 2 deletions src/Tasks.UnitTests/RARPrecomputedCache_Tests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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[]
{
Expand Down Expand Up @@ -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[]
{
Expand Down
Loading
Loading