diff --git a/src/Tasks.UnitTests/WriteCodeFragment_Tests.cs b/src/Tasks.UnitTests/WriteCodeFragment_Tests.cs
index 22f43163eb1..e72f97b3fbd 100644
--- a/src/Tasks.UnitTests/WriteCodeFragment_Tests.cs
+++ b/src/Tasks.UnitTests/WriteCodeFragment_Tests.cs
@@ -5,6 +5,7 @@
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
+using Microsoft.Build.Framework;
using Microsoft.Build.Shared;
using Microsoft.Build.Tasks;
using Microsoft.Build.Utilities;
@@ -27,6 +28,7 @@ public class WriteCodeFragment_Tests
public void InvalidLanguage()
{
WriteCodeFragment task = new WriteCodeFragment();
+ task.TaskEnvironment = TaskEnvironmentHelper.CreateForTest();
MockEngine engine = new MockEngine(true);
task.BuildEngine = engine;
task.Language = "xx";
@@ -44,6 +46,7 @@ public void InvalidLanguage()
public void NoLanguage()
{
WriteCodeFragment task = new WriteCodeFragment();
+ task.TaskEnvironment = TaskEnvironmentHelper.CreateForTest();
MockEngine engine = new MockEngine(true);
task.BuildEngine = engine;
task.OutputFile = new TaskItem("foo");
@@ -60,6 +63,7 @@ public void NoLanguage()
public void NoFileOrDirectory()
{
WriteCodeFragment task = new WriteCodeFragment();
+ task.TaskEnvironment = TaskEnvironmentHelper.CreateForTest();
MockEngine engine = new MockEngine(true);
task.BuildEngine = engine;
task.Language = "c#";
@@ -76,6 +80,7 @@ public void NoFileOrDirectory()
public void CombineFileDirectory()
{
WriteCodeFragment task = new WriteCodeFragment();
+ task.TaskEnvironment = TaskEnvironmentHelper.CreateForTest();
MockEngine engine = new MockEngine(true);
task.BuildEngine = engine;
task.Language = "c#";
@@ -145,6 +150,7 @@ public void FileNameNoDirectory()
using TestEnvironment env = TestEnvironment.Create();
var file = env.ExpectFile(Directory.GetCurrentDirectory(), ".tmp");
WriteCodeFragment task = new WriteCodeFragment();
+ task.TaskEnvironment = TaskEnvironmentHelper.CreateForTest();
MockEngine engine = new MockEngine(true);
task.BuildEngine = engine;
task.Language = "c#";
@@ -167,6 +173,7 @@ public void FileNameNoDirectory()
public void DirectoryAndRootedFile()
{
WriteCodeFragment task = new WriteCodeFragment();
+ task.TaskEnvironment = TaskEnvironmentHelper.CreateForTest();
MockEngine engine = new MockEngine(true);
task.BuildEngine = engine;
task.Language = "c#";
@@ -202,6 +209,7 @@ public void NoAttributesShouldEmitNoFile()
}
WriteCodeFragment task = new WriteCodeFragment();
+ task.TaskEnvironment = TaskEnvironmentHelper.CreateForTest();
MockEngine engine = new MockEngine(true);
task.BuildEngine = engine;
task.Language = "c#";
@@ -229,6 +237,7 @@ public void NoAttributesShouldEmitNoFile2()
}
WriteCodeFragment task = new WriteCodeFragment();
+ task.TaskEnvironment = TaskEnvironmentHelper.CreateForTest();
MockEngine engine = new MockEngine(true);
task.BuildEngine = engine;
task.Language = "c#";
@@ -248,6 +257,7 @@ public void NoAttributesShouldEmitNoFile2()
public void InvalidFilePath()
{
WriteCodeFragment task = new WriteCodeFragment();
+ task.TaskEnvironment = TaskEnvironmentHelper.CreateForTest();
MockEngine engine = new MockEngine(true);
task.BuildEngine = engine;
task.Language = "c#";
@@ -266,6 +276,7 @@ public void InvalidFilePath()
public void InvalidDirectoryPath()
{
WriteCodeFragment task = new WriteCodeFragment();
+ task.TaskEnvironment = TaskEnvironmentHelper.CreateForTest();
MockEngine engine = new MockEngine(true);
task.BuildEngine = engine;
task.Language = "c#";
@@ -288,6 +299,7 @@ public void OneAttributeNoParams()
try
{
WriteCodeFragment task = new WriteCodeFragment();
+ task.TaskEnvironment = TaskEnvironmentHelper.CreateForTest();
MockEngine engine = new MockEngine(true);
task.BuildEngine = engine;
TaskItem attribute = new TaskItem("System.AssemblyTrademarkAttribute");
@@ -317,6 +329,7 @@ public void OneAttributeNoParams()
public void OneAttributeNoParamsVb()
{
WriteCodeFragment task = new WriteCodeFragment();
+ task.TaskEnvironment = TaskEnvironmentHelper.CreateForTest();
MockEngine engine = new MockEngine(true);
task.BuildEngine = engine;
TaskItem attribute = new TaskItem("System.AssemblyTrademarkAttribute");
@@ -340,6 +353,7 @@ public void OneAttributeNoParamsVb()
public void TwoAttributes()
{
WriteCodeFragment task = new WriteCodeFragment();
+ task.TaskEnvironment = TaskEnvironmentHelper.CreateForTest();
MockEngine engine = new MockEngine(true);
task.BuildEngine = engine;
TaskItem attribute1 = new TaskItem("AssemblyTrademarkAttribute");
@@ -369,6 +383,7 @@ public void TwoAttributes()
public void ToDirectory()
{
WriteCodeFragment task = new WriteCodeFragment();
+ task.TaskEnvironment = TaskEnvironmentHelper.CreateForTest();
MockEngine engine = new MockEngine(true);
task.BuildEngine = engine;
TaskItem attribute = new TaskItem("System.AssemblyTrademarkAttribute");
@@ -406,6 +421,55 @@ public void ToDirectoryAndDirectoryDoesNotExist()
Assert.Equal(".cs", task.OutputFile.ItemSpec.Substring(task.OutputFile.ItemSpec.Length - 3));
}
+ ///
+ /// When OutputDirectory is relative and OutputFile is not specified, the resulting OutputFile should be relative.
+ ///
+ [Fact]
+ public void RelativeOutputDirectoryProducesRelativeOutputFile()
+ {
+ using TestEnvironment env = TestEnvironment.Create();
+
+ // Create an actual folder and get a relative path to it
+ string absoluteFolder = env.CreateFolder().Path;
+ string relativeFolder = Path.GetFileName(absoluteFolder);
+
+ // Change current directory to the parent so the relative path works
+ string originalDir = Directory.GetCurrentDirectory();
+ try
+ {
+ Directory.SetCurrentDirectory(Path.GetDirectoryName(absoluteFolder));
+
+ WriteCodeFragment task = new WriteCodeFragment();
+ task.TaskEnvironment = TaskEnvironmentHelper.CreateForTest();
+ MockEngine engine = new MockEngine(true);
+ task.BuildEngine = engine;
+ TaskItem attribute = new TaskItem("System.AssemblyTrademarkAttribute");
+ task.AssemblyAttributes = new TaskItem[] { attribute };
+ task.Language = "c#";
+ task.OutputDirectory = new TaskItem(relativeFolder);
+ bool result = task.Execute();
+
+ result.ShouldBeTrue(engine.Log);
+
+ // The output file should be relative (not rooted) since OutputDirectory was relative
+ Path.IsPathRooted(task.OutputFile.ItemSpec).ShouldBeFalse("OutputFile should be relative when OutputDirectory is relative");
+
+ // The output file should start with the relative folder name
+ task.OutputFile.ItemSpec.ShouldStartWith(relativeFolder);
+
+ // Cleanup the generated file
+ string absoluteOutputFile = Path.Combine(Path.GetDirectoryName(absoluteFolder), task.OutputFile.ItemSpec);
+ if (File.Exists(absoluteOutputFile))
+ {
+ File.Delete(absoluteOutputFile);
+ }
+ }
+ finally
+ {
+ Directory.SetCurrentDirectory(originalDir);
+ }
+ }
+
///
/// Regular case
///
@@ -417,6 +481,7 @@ public void OneAttributeTwoParams()
try
{
WriteCodeFragment task = new WriteCodeFragment();
+ task.TaskEnvironment = TaskEnvironmentHelper.CreateForTest();
MockEngine engine = new MockEngine(true);
task.BuildEngine = engine;
TaskItem attribute = new TaskItem("AssemblyTrademarkAttribute");
@@ -448,6 +513,7 @@ public void OneAttributeTwoParams()
public void OneAttributeTwoParamsSameName()
{
WriteCodeFragment task = new WriteCodeFragment();
+ task.TaskEnvironment = TaskEnvironmentHelper.CreateForTest();
MockEngine engine = new MockEngine(true);
task.BuildEngine = engine;
TaskItem attribute = new TaskItem("AssemblyTrademarkAttribute");
@@ -471,6 +537,7 @@ public void OneAttributeTwoParamsSameName()
public void OneAttributePositionalParamInvalidSuffix()
{
WriteCodeFragment task = new WriteCodeFragment();
+ task.TaskEnvironment = TaskEnvironmentHelper.CreateForTest();
MockEngine engine = new MockEngine(true);
task.BuildEngine = engine;
TaskItem attribute = new TaskItem("AssemblyTrademarkAttribute");
@@ -494,6 +561,7 @@ public void OneAttributePositionalParamInvalidSuffix()
public void OneAttributeTwoPositionalParams()
{
WriteCodeFragment task = new WriteCodeFragment();
+ task.TaskEnvironment = TaskEnvironmentHelper.CreateForTest();
MockEngine engine = new MockEngine(true);
task.BuildEngine = engine;
TaskItem attribute = new TaskItem("AssemblyTrademarkAttribute");
@@ -518,6 +586,7 @@ public void OneAttributeTwoPositionalParams()
public void OneAttributeTwoPositionalParamsWithSameValue()
{
WriteCodeFragment task = new WriteCodeFragment();
+ task.TaskEnvironment = TaskEnvironmentHelper.CreateForTest();
MockEngine engine = new MockEngine(true);
task.BuildEngine = engine;
TaskItem attribute = new TaskItem("AssemblyMetadataAttribute");
@@ -550,6 +619,7 @@ public void MultilineAttributeCSharp()
var multilineString = String.Join(Environment.NewLine, lines);
WriteCodeFragment task = new WriteCodeFragment();
+ task.TaskEnvironment = TaskEnvironmentHelper.CreateForTest();
MockEngine engine = new MockEngine(true);
task.BuildEngine = engine;
TaskItem attribute = new TaskItem("System.Reflection.AssemblyDescriptionAttribute");
@@ -586,6 +656,7 @@ public void MultilineAttributeVB()
var multilineString = String.Join(Environment.NewLine, lines);
WriteCodeFragment task = new WriteCodeFragment();
+ task.TaskEnvironment = TaskEnvironmentHelper.CreateForTest();
MockEngine engine = new MockEngine(true);
task.BuildEngine = engine;
TaskItem attribute = new TaskItem("System.Reflection.AssemblyDescriptionAttribute");
@@ -619,6 +690,7 @@ public void MultilineAttributeVB()
public void OneAttributeSkippedPositionalParams()
{
WriteCodeFragment task = new WriteCodeFragment();
+ task.TaskEnvironment = TaskEnvironmentHelper.CreateForTest();
MockEngine engine = new MockEngine(true);
task.BuildEngine = engine;
TaskItem attribute = new TaskItem("AssemblyTrademarkAttribute");
@@ -642,6 +714,7 @@ public void OneAttributeSkippedPositionalParams()
public void InvalidNumber()
{
WriteCodeFragment task = new WriteCodeFragment();
+ task.TaskEnvironment = TaskEnvironmentHelper.CreateForTest();
MockEngine engine = new MockEngine(true);
task.BuildEngine = engine;
TaskItem attribute = new TaskItem("AssemblyTrademarkAttribute");
@@ -665,6 +738,7 @@ public void InvalidNumber()
public void NoNumber()
{
WriteCodeFragment task = new WriteCodeFragment();
+ task.TaskEnvironment = TaskEnvironmentHelper.CreateForTest();
MockEngine engine = new MockEngine(true);
task.BuildEngine = engine;
TaskItem attribute = new TaskItem("AssemblyTrademarkAttribute");
@@ -688,6 +762,7 @@ public void NoNumber()
public void OneAttributePositionalAndNamedParams()
{
WriteCodeFragment task = new WriteCodeFragment();
+ task.TaskEnvironment = TaskEnvironmentHelper.CreateForTest();
MockEngine engine = new MockEngine(true);
task.BuildEngine = engine;
TaskItem attribute = new TaskItem("AssemblyTrademarkAttribute");
@@ -721,6 +796,7 @@ public void OneAttributePositionalAndNamedParams()
public void OneAttributePositionalAndNamedParamsVisualBasic()
{
WriteCodeFragment task = new WriteCodeFragment();
+ task.TaskEnvironment = TaskEnvironmentHelper.CreateForTest();
MockEngine engine = new MockEngine(true);
task.BuildEngine = engine;
TaskItem attribute = new TaskItem("AssemblyTrademarkAttribute");
@@ -1062,6 +1138,7 @@ public void InferredTypeFallsBackToStringWhenTypeConversionFails()
public void MessageDisplayPositionalParameterNameWhenAttributeNotFound()
{
WriteCodeFragment task = new WriteCodeFragment();
+ task.TaskEnvironment = TaskEnvironmentHelper.CreateForTest();
MockEngine engine = new MockEngine(true);
task.BuildEngine = engine;
TaskItem attribute = new TaskItem("System.TheAttributeCannotFound");
@@ -1101,6 +1178,7 @@ private WriteCodeFragment CreateTask(string language, TaskItem outputDirectory,
{
return new WriteCodeFragment()
{
+ TaskEnvironment = TaskEnvironmentHelper.CreateForTest(),
Language = language,
OutputDirectory = outputDirectory,
OutputFile = outputFile,
@@ -1218,6 +1296,7 @@ public void CommentIsInvariantCulture(string cultureName)
System.Globalization.CultureInfo.CurrentUICulture = new System.Globalization.CultureInfo(cultureName);
WriteCodeFragment task = new WriteCodeFragment();
+ task.TaskEnvironment = TaskEnvironmentHelper.CreateForTest();
MockEngine engine = new MockEngine(true);
task.BuildEngine = engine;
TaskItem attribute = new TaskItem("System.AssemblyVersionAttribute");
diff --git a/src/Tasks/WriteCodeFragment.cs b/src/Tasks/WriteCodeFragment.cs
index cf3d673835f..c0f58314d0f 100644
--- a/src/Tasks/WriteCodeFragment.cs
+++ b/src/Tasks/WriteCodeFragment.cs
@@ -30,8 +30,11 @@ namespace Microsoft.Build.Tasks
///
/// Currently only supports writing .NET attributes.
///
- public class WriteCodeFragment : TaskExtension
+ [MSBuildMultiThreadableTask]
+ public class WriteCodeFragment : TaskExtension, IMultiThreadableTask
{
+ ///
+ public TaskEnvironment TaskEnvironment { get; set; }
private const string TypeNameSuffix = "_TypeName";
private const string IsLiteralSuffix = "_IsLiteral";
private static readonly string[] NamespaceImports = ["System", "System.Reflection"];
@@ -104,6 +107,7 @@ public override bool Execute()
return true;
}
+ AbsolutePath outputFilePath = default;
try
{
if (OutputFile != null && OutputDirectory != null && !Path.IsPathRooted(OutputFile.ItemSpec))
@@ -111,16 +115,25 @@ public override bool Execute()
OutputFile = new TaskItem(Path.Combine(OutputDirectory.ItemSpec, OutputFile.ItemSpec));
}
- OutputFile ??= new TaskItem(FileUtilities.GetTemporaryFile(OutputDirectory.ItemSpec, null, extension));
+ if (OutputFile != null)
+ {
+ outputFilePath = TaskEnvironment.GetAbsolutePath(OutputFile.ItemSpec);
+ }
+ else
+ {
+ AbsolutePath outputDirectoryPath = TaskEnvironment.GetAbsolutePath(OutputDirectory.ItemSpec);
+ outputFilePath = new AbsolutePath(FileUtilities.GetTemporaryFile(outputDirectoryPath, null, extension));
+ OutputFile = new TaskItem(Path.Combine(OutputDirectory.ItemSpec, Path.GetFileName(outputFilePath.Value)));
+ }
- FileUtilities.EnsureDirectoryExists(Path.GetDirectoryName(OutputFile.ItemSpec));
+ FileUtilities.EnsureDirectoryExists(Path.GetDirectoryName(outputFilePath));
- File.WriteAllText(OutputFile.ItemSpec, code); // Overwrites file if it already exists (and can be overwritten)
+ File.WriteAllText(outputFilePath, code); // Overwrites file if it already exists (and can be overwritten)
}
catch (Exception ex) when (ExceptionHandling.IsIoRelatedException(ex))
{
string itemSpec = OutputFile?.ItemSpec ?? String.Empty;
- string lockedFileMessage = LockCheck.GetLockedFileMessage(itemSpec);
+ string lockedFileMessage = LockCheck.GetLockedFileMessage(outputFilePath.Value ?? itemSpec);
Log.LogErrorWithCodeFromResources("WriteCodeFragment.CouldNotWriteOutput", itemSpec, ex.Message, lockedFileMessage);
return false;
}