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