Skip to content
Draft
Show file tree
Hide file tree
Changes from 21 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
88 changes: 88 additions & 0 deletions src/Tasks.UnitTests/SGen_Tests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,12 @@
// 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;

Expand Down Expand Up @@ -273,6 +276,91 @@ public void TestReferencesCommandLine()

Assert.Equal(targetCommandLine, commandLine);
}

/// <summary>
/// Verifies that GenerateFullPathToTool returns an absolute path (or null)
/// when called with a multithreaded TaskEnvironment, validating the
/// TaskEnvironment.GetAbsolutePath() integration.
/// </summary>
[Fact]
public void GenerateFullPathToTool_ReturnsAbsolutePathOrNull()
{
string projectDir = Path.GetTempPath();
using var driver = new MultiThreadedTaskEnvironmentDriver(projectDir);
var taskEnv = new TaskEnvironment(driver);

TestableSGen t = new TestableSGen();
t.TaskEnvironment = taskEnv;
t.BuildEngine = new MockEngine();

string result = t.CallGenerateFullPathToTool();

if (result is not null)
{
Path.IsPathRooted(result).ShouldBeTrue(
$"GenerateFullPathToTool should return an absolute path, got: {result}");
}
}

/// <summary>
/// Verifies that the GetProcessStartInfo routes through the multithreadable path
/// when TaskEnvironment uses MultiThreadedTaskEnvironmentDriver,
/// and that the working directory comes from the TaskEnvironment.
/// </summary>
[Fact]
public void GetProcessStartInfo_UsesTaskEnvironmentWorkingDirectory()
{
string expectedWorkingDir = Path.GetTempPath().TrimEnd(Path.DirectorySeparatorChar);
using var driver = new MultiThreadedTaskEnvironmentDriver(expectedWorkingDir);
var taskEnv = new TaskEnvironment(driver);

TestableSGen t = new TestableSGen();
t.TaskEnvironment = taskEnv;
t.BuildEngine = new MockEngine();

ProcessStartInfo startInfo = t.CallGetProcessStartInfo(@"C:\test\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 var env = TestEnvironment.Create();
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();
t.BuildAssemblyPath = subDir; // relative path

string result = t.BuildAssemblyPath;

Path.IsPathRooted(result).ShouldBeTrue(
"BuildAssemblyPath should return an absolute path resolved via TaskEnvironment");
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
}
}
40 changes: 21 additions & 19 deletions src/Tasks/SGen.cs
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@
}

#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 @@
/// <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,10 @@
string thisPath;
try
{
thisPath = Path.GetFullPath(_buildAssemblyPath);
Path.GetFullPath(_buildAssemblyPath);
thisPath = !string.IsNullOrEmpty(_buildAssemblyPath)
? TaskEnvironment.GetAbsolutePath(_buildAssemblyPath).GetCanonicalForm()
: _buildAssemblyPath;
}
catch (Exception e) when (ExceptionHandling.IsIoRelatedException(e))
{
Expand Down Expand Up @@ -255,16 +260,9 @@
}
}

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 AssemblyFullPathv => new AbsolutePath(Path.Combine(BuildAssemblyPath, BuildAssemblyName));

public string SdkToolsPath
{
Expand Down Expand Up @@ -307,17 +305,21 @@

// 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 +332,7 @@
{
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 +367,11 @@
{
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);

Check failure on line 374 in src/Tasks/SGen.cs

View check run for this annotation

Azure Pipelines / msbuild-pr (Linux Core)

src/Tasks/SGen.cs#L374

src/Tasks/SGen.cs(374,72): error CS0103: (NETCORE_ENGINEERING_TELEMETRY=Build) The name 'AssemblyFullPath' does not exist in the current context

Check failure on line 374 in src/Tasks/SGen.cs

View check run for this annotation

Azure Pipelines / msbuild-pr (Linux Core)

src/Tasks/SGen.cs#L374

src/Tasks/SGen.cs(374,72): error CS0103: (NETCORE_ENGINEERING_TELEMETRY=Build) The name 'AssemblyFullPath' does not exist in the current context

Check failure on line 374 in src/Tasks/SGen.cs

View check run for this annotation

Azure Pipelines / msbuild-pr (Linux Core Multithreaded Mode)

src/Tasks/SGen.cs#L374

src/Tasks/SGen.cs(374,72): error CS0103: (NETCORE_ENGINEERING_TELEMETRY=Build) The name 'AssemblyFullPath' does not exist in the current context

Check failure on line 374 in src/Tasks/SGen.cs

View check run for this annotation

Azure Pipelines / msbuild-pr (Linux Core Multithreaded Mode)

src/Tasks/SGen.cs#L374

src/Tasks/SGen.cs(374,72): error CS0103: (NETCORE_ENGINEERING_TELEMETRY=Build) The name 'AssemblyFullPath' does not exist in the current context

Check failure on line 374 in src/Tasks/SGen.cs

View check run for this annotation

Azure Pipelines / msbuild-pr (macOS Core)

src/Tasks/SGen.cs#L374

src/Tasks/SGen.cs(374,72): error CS0103: (NETCORE_ENGINEERING_TELEMETRY=Build) The name 'AssemblyFullPath' does not exist in the current context

Check failure on line 374 in src/Tasks/SGen.cs

View check run for this annotation

Azure Pipelines / msbuild-pr (macOS Core)

src/Tasks/SGen.cs#L374

src/Tasks/SGen.cs(374,72): error CS0103: (NETCORE_ENGINEERING_TELEMETRY=Build) The name 'AssemblyFullPath' does not exist in the current context

Check failure on line 374 in src/Tasks/SGen.cs

View check run for this annotation

Azure Pipelines / msbuild-pr

src/Tasks/SGen.cs#L374

src/Tasks/SGen.cs(374,72): error CS0103: (NETCORE_ENGINEERING_TELEMETRY=Build) The name 'AssemblyFullPath' does not exist in the current context

Check failure on line 374 in src/Tasks/SGen.cs

View check run for this annotation

Azure Pipelines / msbuild-pr

src/Tasks/SGen.cs#L374

src/Tasks/SGen.cs(374,72): error CS0103: (NETCORE_ENGINEERING_TELEMETRY=Build) The name 'AssemblyFullPath' does not exist in the current context

Check failure on line 374 in src/Tasks/SGen.cs

View check run for this annotation

Azure Pipelines / msbuild-pr

src/Tasks/SGen.cs#L374

src/Tasks/SGen.cs(374,72): error CS0103: (NETCORE_ENGINEERING_TELEMETRY=Build) The name 'AssemblyFullPath' does not exist in the current context

Check failure on line 374 in src/Tasks/SGen.cs

View check run for this annotation

Azure Pipelines / msbuild-pr

src/Tasks/SGen.cs#L374

src/Tasks/SGen.cs(374,72): error CS0103: (NETCORE_ENGINEERING_TELEMETRY=Build) The name 'AssemblyFullPath' does not exist in the current context

commandLineBuilder.AppendWhenTrue("/proxytypes", Bag, "UseProxyTypes");

Expand Down Expand Up @@ -441,11 +443,11 @@
// 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