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; }