Skip to content

Commit 995a3dc

Browse files
YuliiaKovalovaCopilotrainersigwaldbaronfel
authored
Add runtime mismatch validation when Runtime is explicitly specified in custom task (#12642)
### Context Currently, in order to have Runtime="Net" executed out-of-proc, you must explicitly specify TaskFactory="TaskHostFactory" in the UsingTask element. ### Changes Made Added runtime mismatch validation when Runtime is explicitly specified to omit TaskFactory specification in UsingTask. ### Regression No ### Risks Low — validation logic only affects cases where Runtime is explicitly set. ### Testing Added a dedicated test. --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Rainer Sigwald <raines@microsoft.com> Co-authored-by: Chet Husk <chusk3@gmail.com>
1 parent 6b217fc commit 995a3dc

File tree

9 files changed

+84
-17
lines changed

9 files changed

+84
-17
lines changed

eng/Versions.props

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
<Project>
44
<Import Project="Version.Details.props" Condition="Exists('Version.Details.props')" />
55
<PropertyGroup>
6-
<VersionPrefix>18.0.1</VersionPrefix><DotNetFinalVersionKind>release</DotNetFinalVersionKind>
6+
<VersionPrefix>18.0.2</VersionPrefix><DotNetFinalVersionKind>release</DotNetFinalVersionKind>
77
<PackageValidationBaselineVersion>17.14.8</PackageValidationBaselineVersion>
88
<AssemblyVersion>15.1.0.0</AssemblyVersion>
99
<PreReleaseVersionLabel>servicing</PreReleaseVersionLabel>

src/Build.UnitTests/Microsoft.Build.Engine.UnitTests.csproj

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,9 @@
134134
<EmbeddedResource Include="TestAssets\ExampleNetTask\TestMSBuildTaskInNet\global.json">
135135
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
136136
</EmbeddedResource>
137+
<EmbeddedResource Include="TestAssets\ExampleNetTask\TestNetTaskWithImplicitParams\global.json">
138+
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
139+
</EmbeddedResource>
137140
<EmbeddedResource Include="TestAssets\ExampleNetTask\TestNetTask\global.json">
138141
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
139142
</EmbeddedResource>

src/Build.UnitTests/NetTaskHost_E2E_Tests.cs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,5 +134,26 @@ public void MSBuildTaskInNetHostTest()
134134
successTestTask.ShouldBeTrue();
135135
testTaskOutput.ShouldContain($"Hello TEST");
136136
}
137+
138+
[WindowsFullFrameworkOnlyFact]
139+
public void NetTaskWithImplicitHostParamsTest()
140+
{
141+
using TestEnvironment env = TestEnvironment.Create(_output);
142+
var dotnetPath = env.GetEnvironmentVariable("DOTNET_ROOT");
143+
144+
string testProjectPath = Path.Combine(TestAssetsRootPath, "ExampleNetTask", "TestNetTaskWithImplicitParams", "TestNetTaskWithImplicitParams.csproj");
145+
146+
string testTaskOutput = RunnerUtilities.ExecBootstrapedMSBuild($"{testProjectPath} -restore -v:n", out bool successTestTask);
147+
148+
if (!successTestTask)
149+
{
150+
_output.WriteLine(testTaskOutput);
151+
}
152+
153+
successTestTask.ShouldBeTrue();
154+
testTaskOutput.ShouldContain($"The task is executed in process: dotnet");
155+
testTaskOutput.ShouldContain($"Process path: {dotnetPath}", customMessage: testTaskOutput);
156+
testTaskOutput.ShouldContain("/nodereuse:False");
157+
}
137158
}
138159
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<TargetFramework>net10.0</TargetFramework>
5+
</PropertyGroup>
6+
7+
<PropertyGroup>
8+
<TestProjectFolder>$([System.IO.Path]::GetFullPath('$([System.IO.Path]::Combine('$(AssemblyLocation)', '..'))'))</TestProjectFolder>
9+
<ExampleTaskPath>$([System.IO.Path]::Combine('$(TestProjectFolder)', '$(TargetFramework)', 'ExampleTask.dll'))</ExampleTaskPath>
10+
</PropertyGroup>
11+
12+
<UsingTask TaskName="ExampleTask" AssemblyFile="$(ExampleTaskPath)" Runtime="NET"/>
13+
14+
<Target Name="TestTask" BeforeTargets="Build">
15+
<ExampleTask />
16+
</Target>
17+
18+
</Project>
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"sdk": {
3+
// This global.json is needed to prevent builds running in tests using the bootstrap layout from walking
4+
// up the repo tree and resolving our sdk.paths, instead of the bootstrap layout's SDK.
5+
// See https://github.com/dotnet/runtime/issues/118488 for details.
6+
"allowPrerelease": true,
7+
"rollForward": "latestMajor"
8+
}
9+
}

src/Build/Instance/TaskFactories/AssemblyTaskFactory.cs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -274,16 +274,19 @@ internal LoadedType InitializeFactory(
274274
ErrorUtilities.VerifyThrowArgumentNull(loadInfo);
275275
VerifyThrowIdentityParametersValid(taskFactoryIdentityParameters, elementLocation, taskName, "Runtime", "Architecture");
276276

277+
bool taskHostParamsMatchCurrentProc = true;
277278
if (taskFactoryIdentityParameters != null)
278279
{
280+
taskHostParamsMatchCurrentProc = TaskHostParametersMatchCurrentProcess(taskFactoryIdentityParameters);
279281
_factoryIdentityParameters = new Dictionary<string, string>(taskFactoryIdentityParameters, StringComparer.OrdinalIgnoreCase);
280282
}
281283

282284
_taskHostFactoryExplicitlyRequested = taskHostExplicitlyRequested;
283285

284286
_isTaskHostFactory = (taskFactoryIdentityParameters != null
285287
&& taskFactoryIdentityParameters.TryGetValue(Constants.TaskHostExplicitlyRequested, out string isTaskHostFactory)
286-
&& isTaskHostFactory.Equals("true", StringComparison.OrdinalIgnoreCase));
288+
&& isTaskHostFactory.Equals("true", StringComparison.OrdinalIgnoreCase))
289+
|| !taskHostParamsMatchCurrentProc;
287290

288291
try
289292
{
@@ -293,7 +296,7 @@ internal LoadedType InitializeFactory(
293296
string assemblyName = loadInfo.AssemblyName ?? Path.GetFileName(loadInfo.AssemblyFile);
294297
using var assemblyLoadsTracker = AssemblyLoadsTracker.StartTracking(targetLoggingContext, AssemblyLoadingContext.TaskRun, assemblyName);
295298

296-
_loadedType = _typeLoader.Load(taskName, loadInfo, _taskHostFactoryExplicitlyRequested);
299+
_loadedType = _typeLoader.Load(taskName, loadInfo, _taskHostFactoryExplicitlyRequested, taskHostParamsMatchCurrentProc);
297300
ProjectErrorUtilities.VerifyThrowInvalidProject(_loadedType != null, elementLocation, "TaskLoadFailure", taskName, loadInfo.AssemblyLocation, String.Empty);
298301
}
299302
catch (TargetInvocationException e)

src/Build/Instance/TaskRegistry.cs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1489,8 +1489,12 @@ private bool GetTaskFactory(TargetLoggingContext targetLoggingContext, ElementLo
14891489

14901490
bool isAssemblyTaskFactory = String.Equals(TaskFactoryAttributeName, AssemblyTaskFactory, StringComparison.OrdinalIgnoreCase);
14911491
bool isTaskHostFactory = String.Equals(TaskFactoryAttributeName, TaskHostFactory, StringComparison.OrdinalIgnoreCase);
1492-
_taskFactoryParameters ??= new();
1493-
TaskFactoryParameters.Add(Constants.TaskHostExplicitlyRequested, isTaskHostFactory.ToString());
1492+
_taskFactoryParameters ??= [];
1493+
1494+
if (isTaskHostFactory)
1495+
{
1496+
TaskFactoryParameters.Add(Constants.TaskHostExplicitlyRequested, isTaskHostFactory.ToString());
1497+
}
14941498

14951499
if (isAssemblyTaskFactory || isTaskHostFactory)
14961500
{

src/Shared/TypeLoader.cs

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -212,9 +212,10 @@ private static Assembly LoadAssemblyUsingMetadataLoadContext(AssemblyLoadInfo as
212212
internal LoadedType Load(
213213
string typeName,
214214
AssemblyLoadInfo assembly,
215-
bool useTaskHost = false)
215+
bool useTaskHost = false,
216+
bool taskHostParamsMatchCurrentProc = true)
216217
{
217-
return GetLoadedType(s_cacheOfLoadedTypesByFilter, typeName, assembly, useTaskHost);
218+
return GetLoadedType(s_cacheOfLoadedTypesByFilter, typeName, assembly, useTaskHost, taskHostParamsMatchCurrentProc);
218219
}
219220

220221
/// <summary>
@@ -227,15 +228,20 @@ internal LoadedType ReflectionOnlyLoad(
227228
string typeName,
228229
AssemblyLoadInfo assembly)
229230
{
230-
return GetLoadedType(s_cacheOfReflectionOnlyLoadedTypesByFilter, typeName, assembly, useTaskHost: false);
231+
return GetLoadedType(s_cacheOfReflectionOnlyLoadedTypesByFilter, typeName, assembly, useTaskHost: false, taskHostParamsMatchCurrentProc: true);
231232
}
232233

233234
/// <summary>
234235
/// Loads the specified type if it exists in the given assembly. If the type name is fully qualified, then a match (if
235236
/// any) is unambiguous; otherwise, if there are multiple types with the same name in different namespaces, the first type
236237
/// found will be returned.
237238
/// </summary>
238-
private LoadedType GetLoadedType(ConcurrentDictionary<Func<Type, object, bool>, ConcurrentDictionary<AssemblyLoadInfo, AssemblyInfoToLoadedTypes>> cache, string typeName, AssemblyLoadInfo assembly, bool useTaskHost)
239+
private LoadedType GetLoadedType(
240+
ConcurrentDictionary<Func<Type, object, bool>, ConcurrentDictionary<AssemblyLoadInfo, AssemblyInfoToLoadedTypes>> cache,
241+
string typeName,
242+
AssemblyLoadInfo assembly,
243+
bool useTaskHost,
244+
bool taskHostParamsMatchCurrentProc)
239245
{
240246
// A given type filter have been used on a number of assemblies, Based on the type filter we will get another dictionary which
241247
// will map a specific AssemblyLoadInfo to a AssemblyInfoToLoadedTypes class which knows how to find a typeName in a given assembly.
@@ -246,7 +252,7 @@ private LoadedType GetLoadedType(ConcurrentDictionary<Func<Type, object, bool>,
246252
AssemblyInfoToLoadedTypes typeNameToType =
247253
loadInfoToType.GetOrAdd(assembly, (_) => new AssemblyInfoToLoadedTypes(_isDesiredType, _));
248254

249-
return typeNameToType.GetLoadedTypeByTypeName(typeName, useTaskHost);
255+
return typeNameToType.GetLoadedTypeByTypeName(typeName, useTaskHost, taskHostParamsMatchCurrentProc);
250256
}
251257

252258
/// <summary>
@@ -316,11 +322,11 @@ internal AssemblyInfoToLoadedTypes(Func<Type, object, bool> typeFilter, Assembly
316322
/// <summary>
317323
/// Determine if a given type name is in the assembly or not. Return null if the type is not in the assembly
318324
/// </summary>
319-
internal LoadedType GetLoadedTypeByTypeName(string typeName, bool useTaskHost)
325+
internal LoadedType GetLoadedTypeByTypeName(string typeName, bool useTaskHost, bool taskHostParamsMatchCurrentProc)
320326
{
321327
ErrorUtilities.VerifyThrowArgumentNull(typeName);
322328

323-
if (useTaskHost && _assemblyLoadInfo.AssemblyFile is not null)
329+
if (ShouldUseMetadataLoadContext(useTaskHost, taskHostParamsMatchCurrentProc))
324330
{
325331
return GetLoadedTypeFromTypeNameUsingMetadataLoadContext(typeName);
326332
}
@@ -374,6 +380,14 @@ internal LoadedType GetLoadedTypeByTypeName(string typeName, bool useTaskHost)
374380
return type != null ? new LoadedType(type, _assemblyLoadInfo, _loadedAssembly ?? type.Assembly, typeof(ITaskItem), loadedViaMetadataLoadContext: false) : null;
375381
}
376382

383+
/// <summary>
384+
/// Determine whether an assembly is likely to be used out of process and thus loaded with a <see cref="MetadataLoadContext"/>.
385+
/// </summary>
386+
/// <param name="useTaskHost">Task Host Parameter was specified explicitly in XML or through environment variable.</param>
387+
/// <param name="taskHostParamsMatchCurrentProc">The parameter defines if Runtime/Architecture explicitly defined in XML match current process.</param>
388+
private bool ShouldUseMetadataLoadContext(bool useTaskHost, bool taskHostParamsMatchCurrentProc) =>
389+
(useTaskHost || !taskHostParamsMatchCurrentProc) && _assemblyLoadInfo.AssemblyFile is not null;
390+
377391
private LoadedType GetLoadedTypeFromTypeNameUsingMetadataLoadContext(string typeName)
378392
{
379393
return _publicTypeNameToLoadedType.GetOrAdd(typeName, typeName =>

src/UnitTests.Shared/TestEnvironment.cs

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -56,11 +56,6 @@ public partial class TestEnvironment : IDisposable
5656
/// (MSBuild_*.txt) in the temp directory and treats their presence as test failures.
5757
/// Set to true to disable this monitoring for tests that expect build failures.
5858
/// </param>
59-
/// <param name="setupDotnetEnvVars">
60-
/// When true, configures .NET-specific environment variables including PATH,
61-
/// DOTNET_ROOT, and DOTNET_HOST_PATH to point to the bootstrap .NET installation.
62-
/// This ensures tests use the correct .NET runtime and SDK versions.
63-
/// </param>
6459
/// <returns>
6560
/// A configured TestEnvironment instance with the specified settings applied.
6661
/// </returns>

0 commit comments

Comments
 (0)