Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
ab5fdde
Migrate Al task to TaskEnvironment API
OvesN Mar 20, 2026
cf9553b
Merge branch 'main' into dev/veronikao/migrate-Al-task-to-TaskEnviron…
OvesN Mar 20, 2026
64509ed
fix typo
OvesN Mar 20, 2026
2585b4e
Merge branch 'dev/veronikao/migrate-Al-task-to-TaskEnvironment-API' o…
OvesN Mar 20, 2026
1742662
Renaming, comment fix, remove nullablity check of TaskEnvironment
OvesN Mar 20, 2026
1d97469
refactor common code
OvesN Mar 23, 2026
23a3ede
Refactor
OvesN Mar 24, 2026
cfd690e
Delete ToolTask_original.txt
JanProvaznik Mar 24, 2026
5f74f0e
Set directory from taskEnvironment only if GetWorkingDirectory() retu…
OvesN Mar 26, 2026
dbaf184
Migrate ToolTask to Task Environment API
OvesN Mar 26, 2026
65f9abc
Merge branch 'main' into dev/veronikao/migrate-Al-task-to-TaskEnviron…
OvesN Mar 26, 2026
828e302
Remove DeleteTempFile_WarningMessage_UsesOriginalPath test
OvesN Mar 27, 2026
aa6ffe2
Remove unnecessary comment formatting changes.
OvesN Mar 27, 2026
2480e38
migrate SGen task to Task environment API
OvesN Mar 27, 2026
8e595bb
Adressing CR comments: WindowsFullFrameworkOnlyFact added to Al tests,
OvesN Mar 30, 2026
c3ae8c9
Merge branch 'dev/veronikao/migrate-Al-task-to-TaskEnvironment-API' i…
OvesN Mar 30, 2026
4d25a42
Path to tool absolutization fix
OvesN Mar 30, 2026
b04ea30
Merge branch 'dev/veronikao/migrate-SGen-Task-to-TaskEnvironment-API'…
OvesN Mar 30, 2026
831794f
Merge branch 'main' into dev/veronikao/migrate-SGen-Task-to-TaskEnvir…
OvesN Apr 8, 2026
1a588f3
Cleanup after merge
OvesN Apr 8, 2026
aaf974e
Cleanu[
OvesN Apr 8, 2026
4210def
test fixes
OvesN Apr 8, 2026
248a7d1
Impove tests and BuildAssemblyPath getter
OvesN Apr 8, 2026
526b770
Merge branch 'main' into dev/veronikao/migrate-SGen-Task-to-TaskEnvir…
OvesN Apr 8, 2026
28a1145
Adress comments, remove Path.getFullPath, fix test
OvesN Apr 8, 2026
4456ddc
Merge branch 'main' into dev/veronikao/migrate-SGen-Task-to-TaskEnvir…
OvesN Apr 8, 2026
cbf5548
Merge branch 'dev/veronikao/migrate-SGen-Task-to-TaskEnvironment-API'…
OvesN Apr 8, 2026
37f9316
Merge branch 'main' into dev/veronikao/migrate-SGen-Task-to-TaskEnvir…
OvesN Apr 9, 2026
d36d438
Pass null or empty value to GetAbsolutePath for BuildAssemblyPath
OvesN Apr 9, 2026
0fbac5a
Merge branch 'dev/veronikao/migrate-SGen-Task-to-TaskEnvironment-API'…
OvesN Apr 9, 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
109 changes: 103 additions & 6 deletions src/Tasks.UnitTests/SGen_Tests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,25 +2,35 @@
// 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

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(@$"
<Project>
<Target Name=""MyTarget"">
<SGen
Expand All @@ -40,9 +50,8 @@ public void TaskFailsOnCore()
/>
</Target>
</Project>");
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
Expand Down Expand Up @@ -273,6 +282,94 @@ public void TestReferencesCommandLine()

Assert.Equal(targetCommandLine, commandLine);
}

/// <summary>
/// Verifies that GenerateFullPathToTool resolves SdkToolsPath relative to the
/// TaskEnvironment's project directory and returns an absolute path, validating
/// the TaskEnvironment.GetAbsolutePath() integration.
/// </summary>
[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);
}

/// <summary>
/// Verifies that in GetProcessStartInfo
/// working directory comes from the TaskEnvironment.
/// </summary>
[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);
}

/// <summary>
/// Verifies that BuildAssemblyPath uses TaskEnvironment.GetAbsolutePath
/// instead of Path.GetFullPath, by providing a relative path that resolves
/// correctly against the TaskEnvironment's project directory.
/// </summary>
[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));
}

/// <summary>
/// Subclass that exposes protected methods for testing without reflection.
/// </summary>
private sealed class TestableSGen : SGen
{
public string CallGenerateFullPathToTool() => GenerateFullPathToTool();

public ProcessStartInfo CallGetProcessStartInfo(string pathToTool, string commandLineCommands, string responseFileSwitch)
=> GetProcessStartInfo(pathToTool, commandLineCommands, responseFileSwitch);
}
#endif
}
}
37 changes: 18 additions & 19 deletions src/Tasks/SGen.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -138,6 +139,7 @@ public override bool Execute()
/// <summary>
/// Genererates a serialization assembly containing XML serializers for the input assembly.
/// </summary>
[MSBuildMultiThreadableTask]
public class SGen : ToolTaskExtension, ISGenTaskContract
{
private string _buildAssemblyPath;
Expand Down Expand Up @@ -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))
{
Expand Down Expand Up @@ -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
{
Expand Down Expand Up @@ -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),
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

in the else branch, the f is null or empty, right? why do we call the FileInfoExists in such case? won't it be always false?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is to fully preserve the previous behavior. Earlier, the code passed the path directly to
SdkToolsPathUtility.FileInfoExists, and if it was null, it would throw an ArgumentNullException.

ProcessorArchitecture.CurrentProcessArchitecture, SdkToolsPath, ToolExe, Log, true);
}

return pathToTool;
return string.IsNullOrEmpty(pathToTool) ? pathToTool : TaskEnvironment.GetAbsolutePath(pathToTool).Value;
}

/// <summary>
Expand All @@ -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;
Expand Down Expand Up @@ -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");

Expand Down Expand Up @@ -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
Expand Down
Loading