Skip to content

Commit 5e982ce

Browse files
OvesNJanProvaznik
andauthored
Migrate GetReferenceAssemblyPaths task to TaskEnvironment API (#13495)
Fixes #13494 ## Context The `GetReferenceAssemblyPaths` task was made thread-safe for multithreaded MSBuild execution. ## Changes Made ### GetReferenceAssemblyPaths.cs - Replaced `static bool? s_net35SP1SentinelAssemblyFound` with `static readonly Lazy<bool>` using `LazyThreadSafetyMode.PublicationOnly` to ensure thread-safe initialization while preserving retry-on-failure behavior. - Routed path resolution through TaskEnvironment.GetAbsolutePath(). ## Testing Unit tests in GetReferencePaths_Tests.cs --------- Co-authored-by: Jan Provazník <janprovaznik@microsoft.com>
1 parent 684c216 commit 5e982ce

2 files changed

Lines changed: 149 additions & 18 deletions

File tree

src/Tasks.UnitTests/GetReferencePaths_Tests.cs

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
using Microsoft.Build.Shared;
88
using Microsoft.Build.Tasks;
99
using Microsoft.Build.Utilities;
10+
using Shouldly;
1011
using Xunit;
1112
using FrameworkNameVersioning = System.Runtime.Versioning.FrameworkName;
1213

@@ -20,6 +21,13 @@ namespace Microsoft.Build.UnitTests
2021
/// </summary>
2122
public sealed class GetReferenceAssmeblyPath_Tests
2223
{
24+
private readonly ITestOutputHelper _output;
25+
26+
public GetReferenceAssmeblyPath_Tests(ITestOutputHelper output)
27+
{
28+
_output = output;
29+
}
30+
2331
/// <summary>
2432
/// Test the case where there is a good target framework moniker passed in.
2533
/// </summary>
@@ -328,6 +336,91 @@ public void TestGeneralFrameworkMonikerGoodWithFrameworkInFallbackPaths()
328336
Assert.Equal(".NET Framework 4.1", displayName);
329337
}
330338
}
339+
340+
/// <summary>
341+
/// Test that a relative RootPath resolved via TaskEnvironment produces the same result as an absolute RootPath.
342+
/// </summary>
343+
[Fact]
344+
public void TestRelativeRootPathProducesSameResultAsAbsolute()
345+
{
346+
using TestEnvironment env = TestEnvironment.Create(_output);
347+
348+
string baseDir = env.DefaultTestDirectory.Path;
349+
string relativeDir = "framework-root";
350+
string absoluteDir = Path.Combine(baseDir, relativeDir);
351+
var framework41Directory = env.CreateFolder(Path.Combine(absoluteDir, Path.Combine("MyFramework", "v4.1") + Path.DirectorySeparatorChar));
352+
var redistListDirectory = env.CreateFolder(Path.Combine(framework41Directory.Path, "RedistList"));
353+
env.CreateFile(redistListDirectory, "FrameworkList.xml",
354+
"<FileList Redist='Microsoft-Windows-CLRCoreComp' Name='.NET Framework 4.1'>" +
355+
"<File AssemblyName='System.Xml' Version='2.0.0.0' PublicKeyToken='b03f5f7f11d50a3a' Culture='Neutral' FileVersion='2.0.50727.208' InGAC='true' />" +
356+
"</FileList >");
357+
358+
// Baseline: absolute RootPath
359+
MockEngine absoluteEngine = new MockEngine();
360+
GetReferenceAssemblyPaths absoluteTask = new GetReferenceAssemblyPaths();
361+
absoluteTask.BuildEngine = absoluteEngine;
362+
absoluteTask.TargetFrameworkMoniker = "MyFramework, Version=v4.1";
363+
absoluteTask.RootPath = absoluteDir;
364+
absoluteTask.Execute();
365+
366+
// Test: relative RootPath with TaskEnvironment
367+
MockEngine relativeEngine = new MockEngine();
368+
GetReferenceAssemblyPaths relativeTask = new GetReferenceAssemblyPaths();
369+
relativeTask.BuildEngine = relativeEngine;
370+
relativeTask.TargetFrameworkMoniker = "MyFramework, Version=v4.1";
371+
relativeTask.TaskEnvironment = TaskEnvironment.CreateWithProjectDirectoryAndEnvironment(baseDir);
372+
relativeTask.RootPath = relativeDir;
373+
relativeTask.Execute();
374+
375+
relativeTask.ReferenceAssemblyPaths.ShouldBe(absoluteTask.ReferenceAssemblyPaths);
376+
relativeTask.TargetFrameworkMonikerDisplayName.ShouldBe(absoluteTask.TargetFrameworkMonikerDisplayName);
377+
relativeEngine.Errors.ShouldBe(0);
378+
}
379+
380+
/// <summary>
381+
/// Test that a relative path in TargetFrameworkFallbackSearchPaths resolved via TaskEnvironment
382+
/// produces the same result as an absolute fallback path.
383+
/// </summary>
384+
[Fact]
385+
public void TestRelativeFallbackSearchPathProducesSameResultAsAbsolute()
386+
{
387+
using TestEnvironment env = TestEnvironment.Create(_output);
388+
389+
string baseDir = env.DefaultTestDirectory.Path;
390+
string relativeDir = "framework-root";
391+
string absoluteDir = Path.Combine(baseDir, relativeDir);
392+
var framework41Directory = env.CreateFolder(Path.Combine(absoluteDir, Path.Combine("MyFramework", "v4.1") + Path.DirectorySeparatorChar));
393+
var redistListDirectory = env.CreateFolder(Path.Combine(framework41Directory.Path, "RedistList"));
394+
env.CreateFile(redistListDirectory, "FrameworkList.xml",
395+
"<FileList Redist='Microsoft-Windows-CLRCoreComp' Name='.NET Framework 4.1'>" +
396+
"<File AssemblyName='System.Xml' Version='2.0.0.0' PublicKeyToken='b03f5f7f11d50a3a' Culture='Neutral' FileVersion='2.0.50727.208' InGAC='true' />" +
397+
"</FileList >");
398+
399+
string nonExistentRoot = Path.Combine(baseDir, "nonexistent");
400+
401+
// Baseline: absolute fallback path
402+
MockEngine absoluteEngine = new MockEngine();
403+
GetReferenceAssemblyPaths absoluteTask = new GetReferenceAssemblyPaths();
404+
absoluteTask.BuildEngine = absoluteEngine;
405+
absoluteTask.TargetFrameworkMoniker = "MyFramework, Version=v4.1";
406+
absoluteTask.RootPath = nonExistentRoot;
407+
absoluteTask.TargetFrameworkFallbackSearchPaths = absoluteDir;
408+
absoluteTask.Execute();
409+
410+
// Test: relative fallback path with TaskEnvironment
411+
MockEngine relativeEngine = new MockEngine();
412+
GetReferenceAssemblyPaths relativeTask = new GetReferenceAssemblyPaths();
413+
relativeTask.BuildEngine = relativeEngine;
414+
relativeTask.TargetFrameworkMoniker = "MyFramework, Version=v4.1";
415+
relativeTask.TaskEnvironment = TaskEnvironment.CreateWithProjectDirectoryAndEnvironment(baseDir);
416+
relativeTask.RootPath = nonExistentRoot;
417+
relativeTask.TargetFrameworkFallbackSearchPaths = relativeDir;
418+
relativeTask.Execute();
419+
420+
relativeTask.ReferenceAssemblyPaths.ShouldBe(absoluteTask.ReferenceAssemblyPaths);
421+
relativeTask.TargetFrameworkMonikerDisplayName.ShouldBe(absoluteTask.TargetFrameworkMonikerDisplayName);
422+
relativeEngine.Errors.ShouldBe(0);
423+
}
331424
}
332425
}
333426
#endif

src/Tasks/GetReferenceAssemblyPaths.cs

Lines changed: 56 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,12 @@
44
using System;
55
using System.Collections.Generic;
66
using Microsoft.Build.Framework;
7+
using Microsoft.Build.Shared;
78
using Microsoft.Build.Utilities;
89
using FrameworkNameVersioning = System.Runtime.Versioning.FrameworkName;
910

1011
#if FEATURE_GAC
11-
using Microsoft.Build.Shared;
12+
using System.Threading;
1213
using SystemProcessorArchitecture = System.Reflection.ProcessorArchitecture;
1314
#endif
1415

@@ -19,8 +20,12 @@ namespace Microsoft.Build.Tasks
1920
/// <summary>
2021
/// Returns the reference assembly paths to the various frameworks
2122
/// </summary>
22-
public class GetReferenceAssemblyPaths : TaskExtension
23+
[MSBuildMultiThreadableTask]
24+
public class GetReferenceAssemblyPaths : TaskExtension, IMultiThreadableTask
2325
{
26+
/// <inheritdoc />
27+
public TaskEnvironment TaskEnvironment { get; set; } = TaskEnvironment.Fallback;
28+
2429
#region Data
2530
#if FEATURE_GAC
2631
/// <summary>
@@ -32,7 +37,23 @@ public class GetReferenceAssemblyPaths : TaskExtension
3237
/// <summary>
3338
/// Cache in a static whether or not we have found the 35sp1sentinel assembly.
3439
/// </summary>
35-
private static bool? s_net35SP1SentinelAssemblyFound;
40+
private static readonly Lazy<bool> s_net35SP1SentinelAssemblyFound = new Lazy<bool>(() =>
41+
{
42+
// get an assemblyname from the string representation of the sentinel assembly name
43+
var sentinelAssemblyName = new AssemblyNameExtension(NET35SP1SentinelAssemblyName);
44+
string path = GlobalAssemblyCache.GetLocation(
45+
sentinelAssemblyName,
46+
SystemProcessorArchitecture.MSIL,
47+
runtimeVersion => "v2.0.50727",
48+
new Version("2.0.57027"),
49+
false,
50+
new FileExists(p => FileUtilities.FileExistsNoThrow(p)),
51+
GlobalAssemblyCache.pathFromFusionName,
52+
GlobalAssemblyCache.gacEnumerator,
53+
false);
54+
55+
return !string.IsNullOrEmpty(path);
56+
}, LazyThreadSafetyMode.PublicationOnly);
3657
#endif
3758

3859
/// <summary>
@@ -144,6 +165,11 @@ public string TargetFrameworkFallbackSearchPaths
144165
/// </summary>
145166
public override bool Execute()
146167
{
168+
AbsolutePath? absoluteRootPath = !string.IsNullOrEmpty(RootPath)
169+
? TaskEnvironment.GetAbsolutePath(RootPath)
170+
: new AbsolutePath(RootPath, ignoreRootedCheck: true);
171+
IList<AbsolutePath> absoluteFallbackSearchPaths = ResolveAbsoluteFallbackSearchPaths(TargetFrameworkFallbackSearchPaths);
172+
147173
FrameworkNameVersioning moniker;
148174
FrameworkNameVersioning monikerWithNoProfile = null;
149175

@@ -169,16 +195,6 @@ public override bool Execute()
169195
if (!BypassFrameworkInstallChecks && moniker.Identifier.Equals(".NETFramework", StringComparison.OrdinalIgnoreCase) &&
170196
moniker.Version.Major < 4)
171197
{
172-
// We have not got a value for whether or not the 35 sentinel assembly has been found
173-
if (!s_net35SP1SentinelAssemblyFound.HasValue)
174-
{
175-
// get an assemblyname from the string representation of the sentinel assembly name
176-
var sentinelAssemblyName = new AssemblyNameExtension(NET35SP1SentinelAssemblyName);
177-
178-
string path = GlobalAssemblyCache.GetLocation(sentinelAssemblyName, SystemProcessorArchitecture.MSIL, runtimeVersion => "v2.0.50727", new Version("2.0.57027"), false, new FileExists(p => FileUtilities.FileExistsNoThrow(p)), GlobalAssemblyCache.pathFromFusionName, GlobalAssemblyCache.gacEnumerator, false);
179-
s_net35SP1SentinelAssemblyFound = !String.IsNullOrEmpty(path);
180-
}
181-
182198
// We did not find the SP1 sentinel assembly in the GAC. Therefore we must assume that SP1 isn't installed
183199
if (!s_net35SP1SentinelAssemblyFound.Value)
184200
{
@@ -195,7 +211,7 @@ public override bool Execute()
195211

196212
try
197213
{
198-
_tfmPaths = GetPaths(RootPath, TargetFrameworkFallbackSearchPaths, moniker);
214+
_tfmPaths = GetPaths(absoluteRootPath, absoluteFallbackSearchPaths, moniker);
199215

200216
if (_tfmPaths?.Count > 0)
201217
{
@@ -206,7 +222,7 @@ public override bool Execute()
206222
// There is no point in generating the full framework paths if profile path could not be found.
207223
if (targetingProfile && _tfmPaths != null)
208224
{
209-
_tfmPathsNoProfile = GetPaths(RootPath, TargetFrameworkFallbackSearchPaths, monikerWithNoProfile);
225+
_tfmPathsNoProfile = GetPaths(absoluteRootPath, absoluteFallbackSearchPaths, monikerWithNoProfile);
210226
}
211227

212228
// The path with out the profile is just the reference assembly paths.
@@ -236,14 +252,16 @@ public override bool Execute()
236252
/// <summary>
237253
/// Generate the set of chained reference assembly paths
238254
/// </summary>
239-
private IList<String> GetPaths(string rootPath, string targetFrameworkFallbackSearchPaths, FrameworkNameVersioning frameworkmoniker)
255+
private IList<String> GetPaths(AbsolutePath? rootPath, IList<AbsolutePath> fallbackSearchPaths, FrameworkNameVersioning frameworkmoniker)
240256
{
257+
string fallbackSearchPathsJoined = string.Join(";", fallbackSearchPaths);
258+
241259
IList<String> pathsToReturn = ToolLocationHelper.GetPathToReferenceAssemblies(
242260
frameworkmoniker.Identifier,
243261
frameworkmoniker.Version.ToString(),
244262
frameworkmoniker.Profile,
245-
rootPath,
246-
targetFrameworkFallbackSearchPaths);
263+
rootPath?.Value,
264+
fallbackSearchPathsJoined);
247265

248266
if (!SuppressNotFoundError)
249267
{
@@ -267,6 +285,26 @@ private IList<String> GetPaths(string rootPath, string targetFrameworkFallbackSe
267285
return pathsToReturn;
268286
}
269287

288+
/// <summary>
289+
/// Resolves each semicolon-separated fallback search path to absolute via TaskEnvironment.
290+
/// </summary>
291+
private IList<AbsolutePath> ResolveAbsoluteFallbackSearchPaths(string fallbackSearchPaths)
292+
{
293+
if (string.IsNullOrEmpty(fallbackSearchPaths))
294+
{
295+
return [];
296+
}
297+
298+
string[] parts = fallbackSearchPaths.Split(MSBuildConstants.SemicolonChar, StringSplitOptions.RemoveEmptyEntries);
299+
var result = new AbsolutePath[parts.Length];
300+
for (int i = 0; i < parts.Length; i++)
301+
{
302+
result[i] = TaskEnvironment.GetAbsolutePath(parts[i]);
303+
}
304+
305+
return result;
306+
}
307+
270308
#endregion
271309
}
272310
}

0 commit comments

Comments
 (0)