diff --git a/src/Tasks.UnitTests/SGen_Tests.cs b/src/Tasks.UnitTests/SGen_Tests.cs
index 2755cc179aa..6c0cf499b1d 100644
--- a/src/Tasks.UnitTests/SGen_Tests.cs
+++ b/src/Tasks.UnitTests/SGen_Tests.cs
@@ -2,11 +2,15 @@
// The .NET Foundation licenses this file to you under the MIT license.
using System;
+using System.Diagnostics;
using System.IO;
using System.Linq;
+using Microsoft.Build.Framework;
using Microsoft.Build.Tasks;
+using Microsoft.Build.Utilities;
using Shouldly;
using Xunit;
+using Xunit.Abstractions;
#nullable disable
@@ -14,13 +18,19 @@ namespace Microsoft.Build.UnitTests
{
public class SGen_Tests
{
+ private readonly ITestOutputHelper _output;
+
+ public SGen_Tests(ITestOutputHelper output)
+ {
+ _output = output;
+ }
+
#if RUNTIME_TYPE_NETCORE
[Fact]
public void TaskFailsOnCore()
{
- using (TestEnvironment testenv = TestEnvironment.Create())
- {
- MockLogger logger = ObjectModelHelpers.BuildProjectExpectFailure(@$"
+ using TestEnvironment testenv = TestEnvironment.Create(_output);
+ MockLogger logger = ObjectModelHelpers.BuildProjectExpectFailure(@$"
");
- logger.ErrorCount.ShouldBe(1);
- logger.Errors.First().Code.ShouldBe("MSB3474");
- }
+ logger.ErrorCount.ShouldBe(1);
+ logger.Errors.First().Code.ShouldBe("MSB3474");
}
#else
internal sealed class SGenExtension : SGen
@@ -273,6 +282,94 @@ public void TestReferencesCommandLine()
Assert.Equal(targetCommandLine, commandLine);
}
+
+ ///
+ /// Verifies that GenerateFullPathToTool resolves SdkToolsPath relative to the
+ /// TaskEnvironment's project directory and returns an absolute path, validating
+ /// the TaskEnvironment.GetAbsolutePath() integration.
+ ///
+ [Fact]
+ public void GenerateFullPathToTool_ResolvesRelativeSdkToolsPathViaTaskEnvironment()
+ {
+ using TestEnvironment env = TestEnvironment.Create(_output);
+ string projectDir = env.CreateFolder().Path;
+ string sdkSubDir = "sdk";
+ string sdkFullDir = Path.Combine(projectDir, sdkSubDir);
+ Directory.CreateDirectory(sdkFullDir);
+ string fakeTool = Path.Combine(sdkFullDir, "sgen.exe");
+ File.WriteAllBytes(fakeTool, []);
+
+ using var driver = new MultiThreadedTaskEnvironmentDriver(projectDir);
+ var taskEnv = new TaskEnvironment(driver);
+
+ TestableSGen t = new TestableSGen();
+ t.TaskEnvironment = taskEnv;
+ t.BuildEngine = new MockEngine(_output);
+ t.SdkToolsPath = sdkSubDir; // relative path — must resolve via TaskEnvironment
+
+ string result = t.CallGenerateFullPathToTool();
+
+ result.ShouldNotBeNull("GenerateFullPathToTool should find the tool via SdkToolsPath");
+ result.ShouldBe(fakeTool);
+ }
+
+ ///
+ /// Verifies that in GetProcessStartInfo
+ /// working directory comes from the TaskEnvironment.
+ ///
+ [Fact]
+ public void GetProcessStartInfo_UsesTaskEnvironmentWorkingDirectory()
+ {
+ using TestEnvironment env = TestEnvironment.Create(_output);
+ string expectedWorkingDir = env.DefaultTestDirectory.Path;
+ using var driver = new MultiThreadedTaskEnvironmentDriver(expectedWorkingDir);
+ var taskEnv = new TaskEnvironment(driver);
+
+ TestableSGen t = new TestableSGen();
+ t.TaskEnvironment = taskEnv;
+ t.BuildEngine = new MockEngine(_output);
+
+ ProcessStartInfo startInfo = t.CallGetProcessStartInfo(Path.Combine(expectedWorkingDir, "sgen.exe"), "/nologo", null);
+
+ startInfo.WorkingDirectory.ShouldBe(expectedWorkingDir);
+ }
+
+ ///
+ /// Verifies that BuildAssemblyPath uses TaskEnvironment.GetAbsolutePath
+ /// instead of Path.GetFullPath, by providing a relative path that resolves
+ /// correctly against the TaskEnvironment's project directory.
+ ///
+ [Fact]
+ public void BuildAssemblyPath_UsesTaskEnvironmentGetAbsolutePath()
+ {
+ using TestEnvironment env = TestEnvironment.Create(_output);
+ string projectDir = env.CreateFolder().Path;
+ string subDir = "output";
+ Directory.CreateDirectory(Path.Combine(projectDir, subDir));
+
+ using var driver = new MultiThreadedTaskEnvironmentDriver(projectDir);
+ var taskEnv = new TaskEnvironment(driver);
+
+ TestableSGen t = new TestableSGen();
+ t.TaskEnvironment = taskEnv;
+ t.BuildEngine = new MockEngine(_output);
+ t.BuildAssemblyPath = subDir; // relative path
+
+ string result = t.BuildAssemblyPath;
+
+ result.ShouldBe(Path.Combine(projectDir, subDir));
+ }
+
+ ///
+ /// Subclass that exposes protected methods for testing without reflection.
+ ///
+ private sealed class TestableSGen : SGen
+ {
+ public string CallGenerateFullPathToTool() => GenerateFullPathToTool();
+
+ public ProcessStartInfo CallGetProcessStartInfo(string pathToTool, string commandLineCommands, string responseFileSwitch)
+ => GetProcessStartInfo(pathToTool, commandLineCommands, responseFileSwitch);
+ }
#endif
}
}
diff --git a/src/Tasks/SGen.cs b/src/Tasks/SGen.cs
index 7be17cf5dc2..427d034d1ab 100644
--- a/src/Tasks/SGen.cs
+++ b/src/Tasks/SGen.cs
@@ -68,6 +68,7 @@ internal interface ISGenTaskContract
}
#if RUNTIME_TYPE_NETCORE
+ [MSBuildMultiThreadableTask]
public class SGen : ToolTaskExtension, ISGenTaskContract
{
#pragma warning disable format // region formatting is different in net7.0 and net472, and cannot be fixed for both
@@ -138,6 +139,7 @@ public override bool Execute()
///
/// Genererates a serialization assembly containing XML serializers for the input assembly.
///
+ [MSBuildMultiThreadableTask]
public class SGen : ToolTaskExtension, ISGenTaskContract
{
private string _buildAssemblyPath;
@@ -170,7 +172,7 @@ public string BuildAssemblyPath
string thisPath;
try
{
- thisPath = Path.GetFullPath(_buildAssemblyPath);
+ thisPath = TaskEnvironment.GetAbsolutePath(_buildAssemblyPath).GetCanonicalForm().Value;
}
catch (Exception e) when (ExceptionHandling.IsIoRelatedException(e))
{
@@ -255,16 +257,9 @@ public string SerializationAssemblyName
}
}
- private string SerializationAssemblyPath
- {
- get
- {
- Debug.Assert(BuildAssemblyPath.Length > 0, "Build assembly path is blank");
- return Path.Combine(BuildAssemblyPath, SerializationAssemblyName);
- }
- }
+ private AbsolutePath SerializationAssemblyPath => new AbsolutePath(Path.Combine(BuildAssemblyPath, SerializationAssemblyName));
- private string AssemblyFullPath => Path.Combine(BuildAssemblyPath, BuildAssemblyName);
+ private AbsolutePath AssemblyFullPath => new AbsolutePath(Path.Combine(BuildAssemblyPath, BuildAssemblyName));
public string SdkToolsPath
{
@@ -307,17 +302,21 @@ protected override string GenerateFullPathToTool()
// If COMPLUS_InstallRoot\COMPLUS_Version are set (the dogfood world), we want to find it there, instead of
// the SDK, which may or may not be installed. The following will look there.
- if (!String.IsNullOrEmpty(Environment.GetEnvironmentVariable("COMPLUS_InstallRoot")) || !String.IsNullOrEmpty(Environment.GetEnvironmentVariable("COMPLUS_Version")))
+ if (!String.IsNullOrEmpty(TaskEnvironment.GetEnvironmentVariable("COMPLUS_InstallRoot")) || !String.IsNullOrEmpty(TaskEnvironment.GetEnvironmentVariable("COMPLUS_Version")))
{
pathToTool = ToolLocationHelper.GetPathToDotNetFrameworkFile(ToolExe, TargetDotNetFrameworkVersion.Latest);
}
- if (String.IsNullOrEmpty(pathToTool) || !FileSystems.Default.FileExists(pathToTool))
+ if (String.IsNullOrEmpty(pathToTool) || !FileSystems.Default.FileExists(TaskEnvironment.GetAbsolutePath(pathToTool)))
{
- pathToTool = SdkToolsPathUtility.GeneratePathToTool(SdkToolsPathUtility.FileInfoExists, ProcessorArchitecture.CurrentProcessArchitecture, SdkToolsPath, ToolExe, Log, true);
+ pathToTool = SdkToolsPathUtility.GeneratePathToTool(
+ f => !string.IsNullOrEmpty(f)
+ ? SdkToolsPathUtility.FileInfoExists(TaskEnvironment.GetAbsolutePath(f))
+ : SdkToolsPathUtility.FileInfoExists(f),
+ ProcessorArchitecture.CurrentProcessArchitecture, SdkToolsPath, ToolExe, Log, true);
}
- return pathToTool;
+ return string.IsNullOrEmpty(pathToTool) ? pathToTool : TaskEnvironment.GetAbsolutePath(pathToTool).Value;
}
///
@@ -330,7 +329,7 @@ protected override bool ValidateParameters()
{
foreach (string reference in References)
{
- if (!FileSystems.Default.FileExists(reference))
+ if (string.IsNullOrEmpty(reference) || !FileSystems.Default.FileExists(TaskEnvironment.GetAbsolutePath(reference)))
{
Log.LogErrorWithCodeFromResources("SGen.ResourceNotFound", reference);
return false;
@@ -365,11 +364,11 @@ protected override string GenerateCommandLineCommands()
{
Debug.Assert(ShouldGenerateSerializer, "GenerateCommandLineCommands() should not be called if ShouldGenerateSerializer is true and SerializationAssembly is null.");
- SerializationAssembly = [new TaskItem(SerializationAssemblyPath)];
+ SerializationAssembly = [new TaskItem(SerializationAssemblyPath.OriginalValue)];
}
// Add the assembly switch
- commandLineBuilder.AppendSwitchIfNotNull("/assembly:", AssemblyFullPath);
+ commandLineBuilder.AppendSwitchIfNotNull("/assembly:", AssemblyFullPath.Value);
commandLineBuilder.AppendWhenTrue("/proxytypes", Bag, "UseProxyTypes");
@@ -441,11 +440,11 @@ protected override string GenerateCommandLineCommands()
// leave the earlier produced assembly around to be propagated by later processes.
catch (UnauthorizedAccessException e)
{
- Log.LogErrorWithCodeFromResources("SGen.CouldNotDeleteSerializer", SerializationAssemblyPath, e.Message);
+ Log.LogErrorWithCodeFromResources("SGen.CouldNotDeleteSerializer", SerializationAssemblyPath.OriginalValue, e.Message);
}
catch (IOException e)
{
- Log.LogErrorWithCodeFromResources("SGen.CouldNotDeleteSerializer", SerializationAssemblyPath, e.Message);
+ Log.LogErrorWithCodeFromResources("SGen.CouldNotDeleteSerializer", SerializationAssemblyPath.OriginalValue, e.Message);
}
// The DirectoryNotFoundException is safely ignorable since that means that there is no
// existing serialization assembly. This would be extremely unlikely anyway because we