diff --git a/src/Build.UnitTests/BackEnd/TaskEnvironment_Tests.cs b/src/Build.UnitTests/BackEnd/TaskEnvironment_Tests.cs index ba18c88ebc4..acb1bb6e832 100644 --- a/src/Build.UnitTests/BackEnd/TaskEnvironment_Tests.cs +++ b/src/Build.UnitTests/BackEnd/TaskEnvironment_Tests.cs @@ -7,6 +7,7 @@ using System.Linq; using Microsoft.Build.BackEnd; using Microsoft.Build.Framework; +using Microsoft.Build.Shared; using Shouldly; using Xunit; @@ -383,5 +384,54 @@ public void TaskEnvironment_GetAbsolutePath_WithInvalidPathChars_ShouldNotThrow( DisposeTaskEnvironment(taskEnvironment); } } + + [Theory] + [MemberData(nameof(EnvironmentTypes))] + public void TaskEnvironment_GetAbsolutePath_WithEmptyPath_ReturnsProjectDirectory(string environmentType) + { + var taskEnvironment = CreateTaskEnvironment(environmentType); + + // Empty path should absolutize to project directory (Path.Combine behavior) + var absolutePath = taskEnvironment.GetAbsolutePath(string.Empty); + + absolutePath.Value.ShouldBe(taskEnvironment.ProjectDirectory.Value); + absolutePath.OriginalValue.ShouldBe(string.Empty); + } + + [Theory] + [MemberData(nameof(EnvironmentTypes))] + public void TaskEnvironment_GetAbsolutePath_WithNullPath_WhenWave18_4Disabled_ReturnsNullPath(string environmentType) + { + using TestEnvironment testEnv = TestEnvironment.Create(); + ChangeWaves.ResetStateForTests(); + testEnv.SetEnvironmentVariable("MSBUILDDISABLEFEATURESFROMVERSION", ChangeWaves.Wave18_4.ToString()); + BuildEnvironmentHelper.ResetInstance_ForUnitTestsOnly(); + + var taskEnvironment = CreateTaskEnvironment(environmentType); + + // When Wave18_4 is disabled, null path returns as-is + var absolutePath = taskEnvironment.GetAbsolutePath(null!); + + absolutePath.Value.ShouldBeNull(); + absolutePath.OriginalValue.ShouldBeNull(); + + ChangeWaves.ResetStateForTests(); + } + + [Theory] + [MemberData(nameof(EnvironmentTypes))] + public void TaskEnvironment_GetAbsolutePath_WithNullPath_WhenWave18_4Enabled_Throws(string environmentType) + { + using TestEnvironment testEnv = TestEnvironment.Create(); + ChangeWaves.ResetStateForTests(); + BuildEnvironmentHelper.ResetInstance_ForUnitTestsOnly(); + + var taskEnvironment = CreateTaskEnvironment(environmentType); + + // When Wave18_4 is enabled, null path should throw + Should.Throw(() => taskEnvironment.GetAbsolutePath(null!)); + + ChangeWaves.ResetStateForTests(); + } } } diff --git a/src/Tasks.UnitTests/Copy_Tests.cs b/src/Tasks.UnitTests/Copy_Tests.cs index eb43a13be6a..9b3bf954125 100644 --- a/src/Tasks.UnitTests/Copy_Tests.cs +++ b/src/Tasks.UnitTests/Copy_Tests.cs @@ -125,7 +125,11 @@ public void Dispose() [Fact] public void CopyWithNoInput() { - var task = new Copy { BuildEngine = new MockEngine(true), }; + var task = new Copy + { + TaskEnvironment = TaskEnvironmentHelper.CreateForTest(), + BuildEngine = new MockEngine(true), + }; task.Execute().ShouldBeTrue(); (task.CopiedFiles == null || task.CopiedFiles.Length == 0).ShouldBeTrue(); (task.DestinationFiles == null || task.DestinationFiles.Length == 0).ShouldBeTrue(); @@ -141,6 +145,7 @@ public void CopyWithMatchingSourceFilesToDestinationFiles() var task = new Copy { + TaskEnvironment = TaskEnvironmentHelper.CreateForTest(), BuildEngine = new MockEngine(true), SourceFiles = new ITaskItem[] { new TaskItem(sourceFile.Path) }, DestinationFiles = new ITaskItem[] { new TaskItem("destination.txt") }, @@ -166,6 +171,7 @@ public void CopyWithSourceFilesToDestinationFolder(bool isDestinationExists) var task = new Copy { + TaskEnvironment = TaskEnvironmentHelper.CreateForTest(), BuildEngine = new MockEngine(true), SourceFiles = new ITaskItem[] { new TaskItem(sourceFile.Path) }, DestinationFolder = new TaskItem(destinationFolder.Path), @@ -209,6 +215,7 @@ public void CopyWithSourceFoldersToDestinationFolder(bool isDestinationExists) var task = new Copy { + TaskEnvironment = TaskEnvironmentHelper.CreateForTest(), BuildEngine = new MockEngine(true), SourceFolders = new ITaskItem[] { new TaskItem(s0Folder.Path), new TaskItem(s1Folder.Path) }, DestinationFolder = new TaskItem(destinationFolder.Path), @@ -235,6 +242,7 @@ public void CopyWithNoSource() var task = new Copy { + TaskEnvironment = TaskEnvironmentHelper.CreateForTest(), BuildEngine = engine, DestinationFolder = new TaskItem(destinationFolder.Path), }; @@ -264,6 +272,7 @@ public void CopyWithMultipleSourceTypes(bool isDestinationExists) var task = new Copy { + TaskEnvironment = TaskEnvironmentHelper.CreateForTest(), BuildEngine = engine, SourceFiles = new ITaskItem[] { new TaskItem(sourceFile.Path) }, SourceFolders = new ITaskItem[] { new TaskItem(sourceFolder.Path) }, @@ -289,6 +298,7 @@ public void CopyWithEmptySourceFiles(ITaskItem[] sourceFiles) var task = new Copy { + TaskEnvironment = TaskEnvironmentHelper.CreateForTest(), BuildEngine = engine, SourceFiles = sourceFiles, DestinationFolder = new TaskItem(destinationFolder.Path), @@ -313,6 +323,7 @@ public void CopyWithEmptySourceFolders(ITaskItem[] sourceFolders) var task = new Copy { + TaskEnvironment = TaskEnvironmentHelper.CreateForTest(), BuildEngine = engine, SourceFolders = sourceFolders, DestinationFolder = new TaskItem(destinationFolder.Path), @@ -337,6 +348,7 @@ public void CopyWithNoDestination(ITaskItem[] destinationFiles) var task = new Copy { + TaskEnvironment = TaskEnvironmentHelper.CreateForTest(), BuildEngine = engine, SourceFiles = new ITaskItem[] { new TaskItem(sourceFile.Path) }, DestinationFiles = destinationFiles, @@ -361,6 +373,7 @@ public void CopyWithMultipleDestinationTypes() var task = new Copy { + TaskEnvironment = TaskEnvironmentHelper.CreateForTest(), BuildEngine = engine, SourceFiles = new ITaskItem[] { new TaskItem(sourceFile.Path) }, DestinationFiles = new ITaskItem[] { new TaskItem("destination.txt") }, @@ -385,6 +398,7 @@ public void CopyWithSourceFoldersAndDestinationFiles() var task = new Copy { + TaskEnvironment = TaskEnvironmentHelper.CreateForTest(), BuildEngine = engine, SourceFiles = new ITaskItem[] { new TaskItem(sourceFile.Path) }, SourceFolders = new ITaskItem[] { new TaskItem(sourceFolder.Path) }, @@ -408,6 +422,7 @@ public void CopyWithDifferentLengthSourceFilesToDestinationFiles() var task = new Copy { + TaskEnvironment = TaskEnvironmentHelper.CreateForTest(), BuildEngine = engine, SourceFiles = new ITaskItem[] { new TaskItem(sourceFile.Path) }, DestinationFiles = new ITaskItem[] { new TaskItem("destination0.txt"), new TaskItem("destination1.txt") }, @@ -433,6 +448,7 @@ public void CopyWithInvalidRetryCount() var task = new Copy { + TaskEnvironment = TaskEnvironmentHelper.CreateForTest(), BuildEngine = engine, SourceFiles = new ITaskItem[] { new TaskItem(sourceFile.Path) }, DestinationFiles = new ITaskItem[] { new TaskItem("destination.txt") }, @@ -459,6 +475,7 @@ public void CopyWithInvalidRetryDelay() var task = new Copy { + TaskEnvironment = TaskEnvironmentHelper.CreateForTest(), BuildEngine = engine, SourceFiles = new ITaskItem[] { new TaskItem(sourceFile.Path) }, DestinationFiles = new ITaskItem[] { new TaskItem("destination.txt") }, @@ -495,6 +512,7 @@ public void DontCopyOverSameFile(bool isUseHardLinks, bool isUseSymbolicLinks, b CopyMonitor m = new CopyMonitor(); Copy t = new Copy { + TaskEnvironment = TaskEnvironmentHelper.CreateForTest(), RetryDelayMilliseconds = 1, // speed up tests! BuildEngine = new MockEngine(_testOutputHelper), SourceFiles = sourceFiles, @@ -543,6 +561,7 @@ public void QuestionCopyFile(bool isUseHardLinks, bool isUseSymbolicLinks, bool CopyMonitor m = new CopyMonitor(); Copy t = new Copy { + TaskEnvironment = TaskEnvironmentHelper.CreateForTest(), RetryDelayMilliseconds = 1, // speed up tests! BuildEngine = new MockEngine(_testOutputHelper), SourceFiles = sourceFiles, @@ -603,6 +622,7 @@ public void QuestionCopyFileSameContent(bool isUseHardLinks, bool isUseSymbolicL CopyMonitor m = new CopyMonitor(); Copy t = new Copy { + TaskEnvironment = TaskEnvironmentHelper.CreateForTest(), RetryDelayMilliseconds = 1, // speed up tests! BuildEngine = new MockEngine(_testOutputHelper), SourceFiles = sourceFiles, @@ -655,6 +675,7 @@ public void QuestionCopyFileNotSameContent(bool isUseHardLinks, bool isUseSymbol CopyMonitor m = new CopyMonitor(); Copy t = new Copy { + TaskEnvironment = TaskEnvironmentHelper.CreateForTest(), RetryDelayMilliseconds = 1, // speed up tests! BuildEngine = new MockEngine(_testOutputHelper), SourceFiles = sourceFiles, @@ -711,6 +732,7 @@ public void DoNotNormallyCopyOverReadOnlyFile(bool isUseHardLinks, bool isUseSym var t = new Copy { + TaskEnvironment = TaskEnvironmentHelper.CreateForTest(), RetryDelayMilliseconds = 1, // speed up tests! BuildEngine = new MockEngine(_testOutputHelper), SourceFiles = sourceFiles, @@ -778,6 +800,7 @@ public void CopyOverReadOnlyFileEnvironmentOverride(bool isUseHardLinks, bool is var t = new Copy { + TaskEnvironment = TaskEnvironmentHelper.CreateForTest(), RetryDelayMilliseconds = 1, // speed up tests! BuildEngine = new MockEngine(_testOutputHelper), SourceFiles = sourceFiles, @@ -845,6 +868,7 @@ public void AlwaysRetryCopyEnvironmentOverride(bool isUseHardLinks, bool isUseSy var t = new Copy { + TaskEnvironment = TaskEnvironmentHelper.CreateForTest(), RetryDelayMilliseconds = 1, // speed up tests! BuildEngine = new MockEngine(_testOutputHelper), SourceFiles = sourceFiles, @@ -912,6 +936,7 @@ public void CopyOverReadOnlyFileParameterIsSet(bool isUseHardLinks, bool isUseSy var t = new Copy { + TaskEnvironment = TaskEnvironmentHelper.CreateForTest(), RetryDelayMilliseconds = 1, // speed up tests! BuildEngine = new MockEngine(_testOutputHelper), SourceFiles = sourceFiles, @@ -985,6 +1010,7 @@ public void CopyOverReadOnlyFileParameterIsSetWithDestinationFolder(bool isUseHa var t = new Copy { + TaskEnvironment = TaskEnvironmentHelper.CreateForTest(), RetryDelayMilliseconds = 1, // speed up tests! BuildEngine = new MockEngine(_testOutputHelper), SourceFiles = sourceFiles, @@ -1051,6 +1077,7 @@ public void DoCopyOverDifferentFile(bool isUseHardLinks, bool isUseSymbolicLinks var t = new Copy { + TaskEnvironment = TaskEnvironmentHelper.CreateForTest(), RetryDelayMilliseconds = 1, // speed up tests! BuildEngine = new MockEngine(_testOutputHelper), SourceFiles = sourceFiles, @@ -1107,7 +1134,8 @@ public void DoCopyOverCopiedFile(bool skipUnchangedFiles, bool isUseHardLinks, b var engine = new MockEngine(_testOutputHelper); var t = new Copy { - RetryDelayMilliseconds = 1, // speed up tests! + TaskEnvironment = TaskEnvironmentHelper.CreateForTest(), + RetryDelayMilliseconds = 1, // speed up tests! BuildEngine = engine, SourceFiles = new[] { new TaskItem(sourceFile) }, DestinationFiles = new[] { new TaskItem(destinationFile) }, @@ -1186,6 +1214,7 @@ public void DoCopyOverNonExistentFile(bool isUseHardLinks, bool isUseSymbolicLin var t = new Copy { + TaskEnvironment = TaskEnvironmentHelper.CreateForTest(), RetryDelayMilliseconds = 1, // speed up tests! BuildEngine = new MockEngine(_testOutputHelper), SourceFiles = sourceFiles, @@ -1225,6 +1254,7 @@ public void DoNotRetryCopyNotSupportedException(bool isUseHardLinks, bool isUseS var engine = new MockEngine(_testOutputHelper); var t = new Copy { + TaskEnvironment = TaskEnvironmentHelper.CreateForTest(), RetryDelayMilliseconds = 1, // speed up tests! BuildEngine = engine, SourceFiles = sourceFiles, @@ -1272,6 +1302,7 @@ public void DoNotRetryCopyNonExistentSourceFile(bool isUseHardLinks, bool isUseS var engine = new MockEngine(_testOutputHelper); var t = new Copy { + TaskEnvironment = TaskEnvironmentHelper.CreateForTest(), RetryDelayMilliseconds = 1, // speed up tests! BuildEngine = engine, SourceFiles = sourceFiles, @@ -1319,6 +1350,7 @@ public void DoNotRetryCopyWhenSourceIsFolder(bool isUseHardLinks, bool isUseSymb var engine = new MockEngine(_testOutputHelper); var t = new Copy { + TaskEnvironment = TaskEnvironmentHelper.CreateForTest(), RetryDelayMilliseconds = 1, // speed up tests! BuildEngine = engine, SourceFiles = sourceFiles, @@ -1362,6 +1394,7 @@ public void DoRetryWhenDestinationLocked(bool isUseHardLinks, bool isUseSymbolic var engine = new MockEngine(_testOutputHelper); var t = new Copy { + TaskEnvironment = TaskEnvironmentHelper.CreateForTest(), RetryDelayMilliseconds = 1, // speed up tests! BuildEngine = engine, SourceFiles = sourceFiles, @@ -1433,6 +1466,7 @@ public void DoNotRetryWhenDestinationLockedDueToAcl(bool isUseHardLinks, bool is var engine = new MockEngine(_testOutputHelper); var t = new Copy { + TaskEnvironment = TaskEnvironmentHelper.CreateForTest(), RetryDelayMilliseconds = 1, // speed up tests! BuildEngine = engine, SourceFiles = new ITaskItem[] { new TaskItem(sourceFile) }, @@ -1488,6 +1522,7 @@ public void DoNotRetryCopyWhenDestinationFolderIsFile(bool isUseHardLinks, bool var engine = new MockEngine(_testOutputHelper); var t = new Copy { + TaskEnvironment = TaskEnvironmentHelper.CreateForTest(), RetryDelayMilliseconds = 1, // speed up tests! BuildEngine = engine, SourceFiles = sourceFiles, @@ -1535,6 +1570,7 @@ public void DoNotRetryCopyWhenDestinationFileIsFolder(bool isUseHardLinks, bool var engine = new MockEngine(_testOutputHelper); var t = new Copy { + TaskEnvironment = TaskEnvironmentHelper.CreateForTest(), RetryDelayMilliseconds = 1, // speed up tests! BuildEngine = engine, SourceFiles = sourceFiles, @@ -1590,6 +1626,7 @@ public void OutputsOnlyIncludeSuccessfulCopies(bool isUseHardLinks, bool isUseSy var engine = new MockEngine(_testOutputHelper); var t = new Copy { + TaskEnvironment = TaskEnvironmentHelper.CreateForTest(), RetryDelayMilliseconds = 1, // speed up tests! BuildEngine = engine, UseHardlinksIfPossible = isUseHardLinks, @@ -1672,6 +1709,7 @@ public void CopyFileOnItself(bool isUseHardLinks, bool isUseSymbolicLinks) var engine = new MockEngine(_testOutputHelper); var t = new Copy { + TaskEnvironment = TaskEnvironmentHelper.CreateForTest(), RetryDelayMilliseconds = 1, // speed up tests! BuildEngine = engine, SourceFiles = new ITaskItem[] { new TaskItem(file) }, @@ -1692,6 +1730,7 @@ public void CopyFileOnItself(bool isUseHardLinks, bool isUseSymbolicLinks) engine = new MockEngine(_testOutputHelper); t = new Copy { + TaskEnvironment = TaskEnvironmentHelper.CreateForTest(), BuildEngine = engine, SourceFiles = new ITaskItem[] { new TaskItem(file) }, DestinationFiles = new ITaskItem[] { new TaskItem(file) }, @@ -1743,6 +1782,7 @@ public void CopyFileOnItself2(bool isUseHardLinks, bool isUseSymbolicLinks) var engine = new MockEngine(_testOutputHelper); var t = new Copy { + TaskEnvironment = TaskEnvironmentHelper.CreateForTest(), RetryDelayMilliseconds = 1, // speed up tests! BuildEngine = engine, SourceFiles = new ITaskItem[] { new TaskItem(file) }, @@ -1797,6 +1837,7 @@ public void CopyFileOnItselfAndFailACopy(bool isUseHardLinks, bool isUseSymbolic var engine = new MockEngine(_testOutputHelper); var t = new Copy { + TaskEnvironment = TaskEnvironmentHelper.CreateForTest(), RetryDelayMilliseconds = 1, // speed up tests! BuildEngine = engine, SourceFiles = new ITaskItem[] { new TaskItem(file), new TaskItem(invalidFile) }, @@ -1858,6 +1899,7 @@ public void CopyToDestinationFolder(bool isUseHardLinks, bool isUseSymbolicLinks var me = new MockEngine(_testOutputHelper); var t = new Copy { + TaskEnvironment = TaskEnvironmentHelper.CreateForTest(), RetryDelayMilliseconds = 1, // speed up tests! BuildEngine = me, SourceFiles = sourceFiles, @@ -1930,6 +1972,7 @@ public void CopyDoubleEscapableFileToDestinationFolder(bool isUseHardLinks, bool var t = new Copy { + TaskEnvironment = TaskEnvironmentHelper.CreateForTest(), RetryDelayMilliseconds = 1, // speed up tests! BuildEngine = new MockEngine(_testOutputHelper), SourceFiles = sourceFiles, @@ -1995,7 +2038,8 @@ public void CopyWithDuplicatesUsingFolder(bool isUseHardLinks, bool isUseSymboli var t = new Copy { - RetryDelayMilliseconds = 1, // speed up tests! + TaskEnvironment = TaskEnvironmentHelper.CreateForTest(), + RetryDelayMilliseconds = 1, // speed up tests! BuildEngine = new MockEngine(_testOutputHelper), SourceFiles = sourceFiles, DestinationFolder = new TaskItem(Path.Combine(tempPath, "foo")), @@ -2017,7 +2061,7 @@ public void CopyWithDuplicatesUsingFolder(bool isUseHardLinks, bool isUseSymboli Assert.Equal(4, t.CopiedFiles.Length); // Copy calls to different destinations can come in any order when running in parallel. - filesActuallyCopied.Select(f => Path.GetFileName(f.Key.Name)).ShouldBe(new[] { "a.cs", "b.cs" }, ignoreOrder: true); + filesActuallyCopied.Select(f => Path.GetFileName(f.Key.Path)).ShouldBe(new[] { "a.cs", "b.cs" }, ignoreOrder: true); ((MockEngine)t.BuildEngine).AssertLogDoesntContain("MSB3026"); // Didn't do retries } @@ -2062,6 +2106,7 @@ public void CopyWithDuplicatesUsingFiles(bool isUseHardLinks, bool isUseSymbolic var t = new Copy { + TaskEnvironment = TaskEnvironmentHelper.CreateForTest(), RetryDelayMilliseconds = 1, // speed up tests! BuildEngine = new MockEngine(_testOutputHelper), SourceFiles = sourceFiles, @@ -2085,16 +2130,16 @@ public void CopyWithDuplicatesUsingFiles(bool isUseHardLinks, bool isUseSymbolic // Copy calls to different destinations can come in any order when running in parallel. string xaPath = Path.Combine(tempPath, "xa.cs"); - var xaCopies = filesActuallyCopied.Where(f => f.Value.Name == xaPath).ToList(); + var xaCopies = filesActuallyCopied.Where(f => f.Value.Path == xaPath).ToList(); Assert.Equal(3, xaCopies.Count); - Assert.Equal(Path.Combine(tempPath, "a.cs"), xaCopies[0].Key.Name); - Assert.Equal(Path.Combine(tempPath, "b.cs"), xaCopies[1].Key.Name); - Assert.Equal(Path.Combine(tempPath, "a.cs"), xaCopies[2].Key.Name); + Assert.Equal(Path.Combine(tempPath, "a.cs"), xaCopies[0].Key.Path); + Assert.Equal(Path.Combine(tempPath, "b.cs"), xaCopies[1].Key.Path); + Assert.Equal(Path.Combine(tempPath, "a.cs"), xaCopies[2].Key.Path); string xbPath = Path.Combine(tempPath, "xb.cs"); - var xbCopies = filesActuallyCopied.Where(f => f.Value.Name == xbPath).ToList(); + var xbCopies = filesActuallyCopied.Where(f => f.Value.Path == xbPath).ToList(); Assert.Single(xbCopies); - Assert.Equal(Path.Combine(tempPath, "a.cs"), xbCopies[0].Key.Name); + Assert.Equal(Path.Combine(tempPath, "a.cs"), xbCopies[0].Key.Path); ((MockEngine)t.BuildEngine).AssertLogDoesntContain("MSB3026"); // Didn't do retries } @@ -2131,6 +2176,7 @@ public void DestinationFilesLengthNotEqualSourceFilesLength(bool isUseHardLinks, var engine = new MockEngine(_testOutputHelper); var t = new Copy { + TaskEnvironment = TaskEnvironmentHelper.CreateForTest(), RetryDelayMilliseconds = 1, // speed up tests! BuildEngine = engine, SourceFiles = new ITaskItem[] { new TaskItem(inFile1), new TaskItem(inFile2) }, @@ -2179,6 +2225,7 @@ public void Regress451057_ExitGracefullyIfPathNameIsTooLong(bool isUseHardLinks, var t = new Copy { + TaskEnvironment = TaskEnvironmentHelper.CreateForTest(), RetryDelayMilliseconds = 1, // speed up tests! BuildEngine = new MockEngine(_testOutputHelper), SourceFiles = sourceFiles, @@ -2217,6 +2264,7 @@ public void Regress451057_ExitGracefullyIfPathNameIsTooLong2(bool isUseHardLinks var t = new Copy { + TaskEnvironment = TaskEnvironmentHelper.CreateForTest(), RetryDelayMilliseconds = 1, // speed up tests! BuildEngine = new MockEngine(_testOutputHelper), SourceFiles = sourceFiles, @@ -2243,7 +2291,8 @@ public void ExitGracefullyOnInvalidPathCharacters(bool isUseHardLinks, bool isUs { var t = new Copy { - RetryDelayMilliseconds = 1, // speed up tests! + TaskEnvironment = TaskEnvironmentHelper.CreateForTest(), + RetryDelayMilliseconds = 1, // speed up tests! BuildEngine = new MockEngine(_testOutputHelper), SourceFiles = new ITaskItem[] { new TaskItem("foo | bar") }, DestinationFolder = new TaskItem("dest"), @@ -2267,7 +2316,8 @@ public void ExitGracefullyOnInvalidPathCharactersInDestinationFolder(bool isUseH { var t = new Copy { - RetryDelayMilliseconds = 1, // speed up tests! + TaskEnvironment = TaskEnvironmentHelper.CreateForTest(), + RetryDelayMilliseconds = 1, // speed up tests! BuildEngine = new MockEngine(_testOutputHelper), SourceFiles = new ITaskItem[] { new TaskItem("foo") }, DestinationFolder = new TaskItem("here | there"), @@ -2291,7 +2341,8 @@ public void InvalidRetryCount() var engine = new MockEngine(true /* log to console */); var t = new Copy { - RetryDelayMilliseconds = 1, // speed up tests! + TaskEnvironment = TaskEnvironmentHelper.CreateForTest(), + RetryDelayMilliseconds = 1, // speed up tests! BuildEngine = engine, SourceFiles = new ITaskItem[] { new TaskItem("c:\\source") }, DestinationFiles = new ITaskItem[] { new TaskItem("c:\\destination") }, @@ -2313,7 +2364,8 @@ public void InvalidRetryDelayCount() var engine = new MockEngine(true /* log to console */); var t = new Copy { - BuildEngine = engine, + TaskEnvironment = TaskEnvironmentHelper.CreateForTest(), + BuildEngine = engine, SourceFiles = new ITaskItem[] { new TaskItem("c:\\source") }, DestinationFiles = new ITaskItem[] { new TaskItem("c:\\destination") }, Retries = 1, @@ -2337,7 +2389,8 @@ public void FailureWithNoRetries(bool isUseHardLinks, bool isUseSymbolicLinks, b var engine = new MockEngine(true /* log to console */); var t = new Copy { - RetryDelayMilliseconds = 1, // speed up tests! + TaskEnvironment = TaskEnvironmentHelper.CreateForTest(), + RetryDelayMilliseconds = 1, // speed up tests! BuildEngine = engine, SourceFiles = new ITaskItem[] { new TaskItem("c:\\source") }, DestinationFiles = new ITaskItem[] { new TaskItem("c:\\destination") }, @@ -2362,6 +2415,7 @@ public void DefaultRetriesIs10() { var t = new Copy { + TaskEnvironment = TaskEnvironmentHelper.CreateForTest(), RetryDelayMilliseconds = 1, // speed up tests! }; @@ -2387,7 +2441,8 @@ public void DefaultNoHardlink() { var t = new Copy { - RetryDelayMilliseconds = 1, // speed up tests! + TaskEnvironment = TaskEnvironmentHelper.CreateForTest(), + RetryDelayMilliseconds = 1, // speed up tests! }; Assert.False(t.UseHardlinksIfPossible); @@ -2404,7 +2459,8 @@ public void SuccessAfterOneRetry(bool isUseHardLinks, bool isUseSymbolicLinks, b var engine = new MockEngine(true /* log to console */); var t = new Copy { - RetryDelayMilliseconds = 0, // Can't really test the delay, but at least try passing in a value + TaskEnvironment = TaskEnvironmentHelper.CreateForTest(), + RetryDelayMilliseconds = 0, // Can't really test the delay, but at least try passing in a value BuildEngine = engine, SourceFiles = new ITaskItem[] { new TaskItem("c:\\source") }, DestinationFiles = new ITaskItem[] { new TaskItem("c:\\destination") }, @@ -2431,7 +2487,8 @@ public void SuccessAfterOneRetryContinueToNextFile(bool isUseHardLinks, bool isU var engine = new MockEngine(true /* log to console */); var t = new Copy { - RetryDelayMilliseconds = 1, // Can't really test the delay, but at least try passing in a value + TaskEnvironment = TaskEnvironmentHelper.CreateForTest(), + RetryDelayMilliseconds = 1, // Can't really test the delay, but at least try passing in a value BuildEngine = engine, SourceFiles = new ITaskItem[] { new TaskItem("c:\\source"), new TaskItem("c:\\source2") }, DestinationFiles = new ITaskItem[] { new TaskItem("c:\\destination"), new TaskItem("c:\\destination2") }, @@ -2448,8 +2505,10 @@ public void SuccessAfterOneRetryContinueToNextFile(bool isUseHardLinks, bool isU engine.AssertLogDoesntContain("MSB3027"); // Copy calls to different destinations can come in any order when running in parallel. - Assert.Contains(copyFunctor.FilesCopiedSuccessfully, f => f.Name == FrameworkFileUtilities.FixFilePath("c:\\source")); - Assert.Contains(copyFunctor.FilesCopiedSuccessfully, f => f.Name == FrameworkFileUtilities.FixFilePath("c:\\source2")); + // Use .OriginalValue to compare against the original input path (before Path.GetFullPath resolution). + // TaskItem normalizes paths via FileUtilities.FixFilePath, so we need to do the same for comparison. + Assert.Contains(copyFunctor.FilesCopiedSuccessfully, f => f.Path.OriginalValue == FrameworkFileUtilities.FixFilePath("c:\\source")); + Assert.Contains(copyFunctor.FilesCopiedSuccessfully, f => f.Path.OriginalValue == FrameworkFileUtilities.FixFilePath("c:\\source2")); } /// @@ -2463,6 +2522,7 @@ public void TooFewRetriesReturnsFalse(bool isUseHardLinks, bool isUseSymbolicLin var engine = new MockEngine(true /* log to console */); var t = new Copy { + TaskEnvironment = TaskEnvironmentHelper.CreateForTest(), RetryDelayMilliseconds = 1, // speed up tests! BuildEngine = engine, SourceFiles = new ITaskItem[] { new TaskItem("c:\\source") }, @@ -2492,6 +2552,7 @@ public void TooFewRetriesThrows(bool isUseHardLinks, bool isUseSymbolicLinks, bo var engine = new MockEngine(true /* log to console */); var t = new Copy { + TaskEnvironment = TaskEnvironmentHelper.CreateForTest(), RetryDelayMilliseconds = 1, // speed up tests! BuildEngine = engine, SourceFiles = new ITaskItem[] { new TaskItem("c:\\source") }, @@ -2524,6 +2585,7 @@ public void ErrorIfLinkFailedCheck(bool isUseHardLinks, bool isUseSymbolicLinks) MockEngine engine = new MockEngine(_testOutputHelper); Copy t = new Copy { + TaskEnvironment = TaskEnvironmentHelper.CreateForTest(), RetryDelayMilliseconds = 1, UseHardlinksIfPossible = isUseHardLinks, UseSymboliclinksIfPossible = isUseSymbolicLinks, @@ -2559,6 +2621,7 @@ public void CopyToDestinationFolderWithHardLinkCheck() var me = new MockEngine(true); var t = new Copy { + TaskEnvironment = TaskEnvironmentHelper.CreateForTest(), RetryDelayMilliseconds = 1, // speed up tests! BuildEngine = me, SourceFiles = sourceFiles, @@ -2649,6 +2712,7 @@ public void CopyToDestinationFolderWithHardLinkFallbackNetwork() var me = new MockEngine(true); var t = new Copy { + TaskEnvironment = TaskEnvironmentHelper.CreateForTest(), RetryDelayMilliseconds = 1, // speed up tests! UseHardlinksIfPossible = true, BuildEngine = me, @@ -2735,6 +2799,7 @@ public void CopyToDestinationFolderWithHardLinkFallbackTooManyLinks() MockEngine me = new MockEngine(true); Copy t = new Copy { + TaskEnvironment = TaskEnvironmentHelper.CreateForTest(), RetryDelayMilliseconds = 1, // speed up tests! UseHardlinksIfPossible = true, BuildEngine = me, @@ -2814,6 +2879,7 @@ public void CopyToDestinationFolderWithSymbolicLinkCheck() var me = new MockEngine(true); var t = new Copy { + TaskEnvironment = TaskEnvironmentHelper.CreateForTest(), RetryDelayMilliseconds = 1, // speed up tests! BuildEngine = me, SourceFiles = sourceFiles, @@ -2878,6 +2944,7 @@ public void CopyWithHardAndSymbolicLinks() MockEngine me = new MockEngine(true); Copy t = new Copy { + TaskEnvironment = TaskEnvironmentHelper.CreateForTest(), RetryDelayMilliseconds = 1, // speed up tests! UseHardlinksIfPossible = true, UseSymboliclinksIfPossible = true, @@ -2909,7 +2976,8 @@ public void InvalidErrorIfLinkFailed() var engine = new MockEngine(true); var t = new Copy { - BuildEngine = engine, + TaskEnvironment = TaskEnvironmentHelper.CreateForTest(), + BuildEngine = engine, SourceFiles = new ITaskItem[] { new TaskItem("c:\\source") }, DestinationFiles = new ITaskItem[] { new TaskItem("c:\\destination") }, UseHardlinksIfPossible = false, @@ -2946,6 +3014,7 @@ public void DoNotCorruptSourceOfLink(bool useHardLink, bool useSymbolicLink) var me = new MockEngine(true); var t = new Copy { + TaskEnvironment = TaskEnvironmentHelper.CreateForTest(), RetryDelayMilliseconds = 1, // speed up tests! BuildEngine = me, SourceFiles = sourceFiles, @@ -2963,6 +3032,7 @@ public void DoNotCorruptSourceOfLink(bool useHardLink, bool useSymbolicLink) t = new Copy { + TaskEnvironment = TaskEnvironmentHelper.CreateForTest(), RetryDelayMilliseconds = 1, // speed up tests! BuildEngine = me, SourceFiles = sourceFiles, @@ -2986,6 +3056,7 @@ public void DoNotCorruptSourceOfLink(bool useHardLink, bool useSymbolicLink) t = new Copy { + TaskEnvironment = TaskEnvironmentHelper.CreateForTest(), RetryDelayMilliseconds = 1, // speed up tests! BuildEngine = me, SourceFiles = sourceFiles, @@ -3124,6 +3195,7 @@ public void CopyToFileWithSameCaseInsensitiveNameAsExistingDirectoryOnUnix() string destFile2 = Path.Combine(outputDir, "app.dll"); Copy t = new Copy(); + t.TaskEnvironment = TaskEnvironmentHelper.CreateForTest(); MockEngine engine = new MockEngine(); t.BuildEngine = engine; t.SourceFiles = new ITaskItem[] { diff --git a/src/Tasks.UnitTests/Delete_Tests.cs b/src/Tasks.UnitTests/Delete_Tests.cs index e523c13816c..85a3629987b 100644 --- a/src/Tasks.UnitTests/Delete_Tests.cs +++ b/src/Tasks.UnitTests/Delete_Tests.cs @@ -24,6 +24,7 @@ public sealed class Delete_Tests public void AttributeForwarding() { Delete t = new Delete(); + t.TaskEnvironment = TaskEnvironmentHelper.CreateForTest(); ITaskItem i = new TaskItem("MyFiles.nonexistent"); i.SetMetadata("Locale", "en-GB"); @@ -59,6 +60,7 @@ public void DeleteWithRetries() var t = new Delete { + TaskEnvironment = TaskEnvironmentHelper.CreateForTest(), RetryDelayMilliseconds = 1, // speed up tests! BuildEngine = new MockEngine(), Files = sourceFiles, @@ -75,6 +77,7 @@ public void DeleteWithRetries() ITaskItem[] duplicateSourceFiles = { sourceItem, sourceItem }; t = new Delete { + TaskEnvironment = TaskEnvironmentHelper.CreateForTest(), RetryDelayMilliseconds = 1, // speed up tests! BuildEngine = new MockEngine(), Files = duplicateSourceFiles, diff --git a/src/Tasks.UnitTests/FileStateTests.cs b/src/Tasks.UnitTests/FileStateTests.cs index 1b23ffaba7d..e51abea01cf 100644 --- a/src/Tasks.UnitTests/FileStateTests.cs +++ b/src/Tasks.UnitTests/FileStateTests.cs @@ -3,6 +3,7 @@ using System; using System.IO; +using Microsoft.Build.Framework; using Microsoft.Build.Shared; using Microsoft.Build.Tasks; using Xunit; @@ -16,30 +17,35 @@ namespace Microsoft.Build.UnitTests /// public class FileStateTests { + /// + /// Helper to create AbsolutePath for tests, bypassing rooted check for test paths. + /// + private static AbsolutePath TestPath(string path) => new AbsolutePath(path, ignoreRootedCheck: true); + [Fact] public void BadNoName() { Assert.Throws(() => { - new FileState(""); + new FileState(TestPath("")); }); } [Fact] public void BadCharsCtorOK() { - new FileState("|"); + new FileState(TestPath("|")); } [Fact] public void BadTooLongCtorOK() { - new FileState(new String('x', 5000)); + new FileState(TestPath(new String('x', 5000))); } [WindowsFullFrameworkOnlyFact(additionalMessage: ".NET Core 2.1+ no longer validates paths: https://github.com/dotnet/corefx/issues/27779#issuecomment-371253486. On Unix there is no invalid file name characters.")] public void BadChars() { - var state = new FileState("|"); + var state = new FileState(TestPath("|")); Assert.Throws(() => { var time = state.LastWriteTime; }); } @@ -48,7 +54,7 @@ public void BadTooLongLastWriteTime() { Helpers.VerifyAssertThrowsSameWay( delegate () { var x = new FileInfo(new String('x', 5000)).LastWriteTime; }, - delegate () { var x = new FileState(new String('x', 5000)).LastWriteTime; }); + delegate () { var x = new FileState(TestPath(new String('x', 5000))).LastWriteTime; }); } [Fact] @@ -60,7 +66,7 @@ public void Exists() { file = FileUtilities.GetTemporaryFile(); FileInfo info = new FileInfo(file); - FileState state = new FileState(file); + FileState state = new FileState(TestPath(file)); Assert.Equal(info.Exists, state.FileExists); } @@ -79,9 +85,9 @@ public void Name() { file = FileUtilities.GetTemporaryFile(); FileInfo info = new FileInfo(file); - FileState state = new FileState(file); + FileState state = new FileState(TestPath(file)); - Assert.Equal(info.FullName, state.Name); + Assert.Equal(info.FullName, state.Path); } finally { @@ -92,7 +98,7 @@ public void Name() [Fact] public void IsDirectoryTrue() { - var state = new FileState(Path.GetTempPath()); + var state = new FileState(TestPath(Path.GetTempPath())); Assert.True(state.IsDirectory); } @@ -106,7 +112,7 @@ public void LastWriteTime() { file = FileUtilities.GetTemporaryFile(); FileInfo info = new FileInfo(file); - FileState state = new FileState(file); + FileState state = new FileState(TestPath(file)); Assert.Equal(info.LastWriteTime, state.LastWriteTime); } @@ -125,7 +131,7 @@ public void LastWriteTimeUtc() { file = FileUtilities.GetTemporaryFile(); FileInfo info = new FileInfo(file); - FileState state = new FileState(file); + FileState state = new FileState(TestPath(file)); Assert.Equal(info.LastWriteTimeUtc, state.LastWriteTimeUtcFast); } @@ -144,7 +150,7 @@ public void Length() { file = FileUtilities.GetTemporaryFile(); FileInfo info = new FileInfo(file); - FileState state = new FileState(file); + FileState state = new FileState(TestPath(file)); Assert.Equal(info.Length, state.Length); } @@ -163,7 +169,7 @@ public void ReadOnly() { file = FileUtilities.GetTemporaryFile(); FileInfo info = new FileInfo(file); - FileState state = new FileState(file); + FileState state = new FileState(TestPath(file)); Assert.Equal(info.IsReadOnly, state.IsReadOnly); } @@ -182,7 +188,7 @@ public void ExistsReset() { file = FileUtilities.GetTemporaryFile(); FileInfo info = new FileInfo(file); - FileState state = new FileState(file); + FileState state = new FileState(TestPath(file)); Assert.Equal(info.Exists, state.FileExists); File.Delete(file); @@ -208,16 +214,16 @@ public void NameReset() { file = FileUtilities.GetTemporaryFile(); FileInfo info = new FileInfo(file); - FileState state = new FileState(file); + FileState state = new FileState(TestPath(file)); - Assert.Equal(info.FullName, state.Name); + Assert.Equal(info.FullName, state.Path); string originalName = info.FullName; string oldFile = file; file = oldFile + "2"; File.Move(oldFile, file); - Assert.Equal(originalName, state.Name); + Assert.Equal(originalName, state.Path); state.Reset(); - Assert.Equal(originalName, state.Name); // Name is from the constructor, didn't change + Assert.Equal(originalName, state.Path); // Name is from the constructor, didn't change } finally { @@ -234,7 +240,7 @@ public void LastWriteTimeReset() { file = FileUtilities.GetTemporaryFile(); FileInfo info = new FileInfo(file); - FileState state = new FileState(file); + FileState state = new FileState(TestPath(file)); Assert.Equal(info.LastWriteTime, state.LastWriteTime); @@ -260,7 +266,7 @@ public void LastWriteTimeUtcReset() { file = FileUtilities.GetTemporaryFile(); FileInfo info = new FileInfo(file); - FileState state = new FileState(file); + FileState state = new FileState(TestPath(file)); Assert.Equal(info.LastWriteTimeUtc, state.LastWriteTimeUtcFast); @@ -288,7 +294,7 @@ public void LengthReset() { file = FileUtilities.GetTemporaryFile(); FileInfo info = new FileInfo(file); - FileState state = new FileState(file); + FileState state = new FileState(TestPath(file)); Assert.Equal(info.Length, state.Length); File.WriteAllText(file, "x"); @@ -313,7 +319,7 @@ public void ReadOnlyReset() { file = FileUtilities.GetTemporaryFile(); FileInfo info = new FileInfo(file); - FileState state = new FileState(file); + FileState state = new FileState(TestPath(file)); Assert.Equal(info.IsReadOnly, state.IsReadOnly); info.IsReadOnly = !info.IsReadOnly; @@ -330,32 +336,32 @@ public void ReadOnlyReset() [Fact] public void ExistsButDirectory() { - Assert.Equal(new FileInfo(Path.GetTempPath()).Exists, new FileState(Path.GetTempPath()).FileExists); - Assert.True(new FileState(Path.GetTempPath()).IsDirectory); + Assert.Equal(new FileInfo(Path.GetTempPath()).Exists, new FileState(TestPath(Path.GetTempPath())).FileExists); + Assert.True(new FileState(TestPath(Path.GetTempPath())).IsDirectory); } [Fact] public void ReadOnlyOnDirectory() { - Assert.Equal(new FileInfo(Path.GetTempPath()).IsReadOnly, new FileState(Path.GetTempPath()).IsReadOnly); + Assert.Equal(new FileInfo(Path.GetTempPath()).IsReadOnly, new FileState(TestPath(Path.GetTempPath())).IsReadOnly); } [Fact] public void LastWriteTimeOnDirectory() { - Assert.Equal(new FileInfo(Path.GetTempPath()).LastWriteTime, new FileState(Path.GetTempPath()).LastWriteTime); + Assert.Equal(new FileInfo(Path.GetTempPath()).LastWriteTime, new FileState(TestPath(Path.GetTempPath())).LastWriteTime); } [Fact] public void LastWriteTimeUtcOnDirectory() { - Assert.Equal(new FileInfo(Path.GetTempPath()).LastWriteTimeUtc, new FileState(Path.GetTempPath()).LastWriteTimeUtcFast); + Assert.Equal(new FileInfo(Path.GetTempPath()).LastWriteTimeUtc, new FileState(TestPath(Path.GetTempPath())).LastWriteTimeUtcFast); } [Fact] public void LengthOnDirectory() { - Helpers.VerifyAssertThrowsSameWay(delegate () { var x = new FileInfo(Path.GetTempPath()).Length; }, delegate () { var x = new FileState(Path.GetTempPath()).Length; }); + Helpers.VerifyAssertThrowsSameWay(delegate () { var x = new FileInfo(Path.GetTempPath()).Length; }, delegate () { var x = new FileState(TestPath(Path.GetTempPath())).Length; }); } [Fact] @@ -365,7 +371,7 @@ public void DoesNotExistLastWriteTime() { string file = Guid.NewGuid().ToString("N"); - Assert.Equal(new FileInfo(file).LastWriteTime, new FileState(file).LastWriteTime); + Assert.Equal(new FileInfo(file).LastWriteTime, new FileState(TestPath(file)).LastWriteTime); } [Fact] @@ -375,7 +381,7 @@ public void DoesNotExistLastWriteTimeUtc() { string file = Guid.NewGuid().ToString("N"); - Assert.Equal(new FileInfo(file).LastWriteTimeUtc, new FileState(file).LastWriteTimeUtcFast); + Assert.Equal(new FileInfo(file).LastWriteTimeUtc, new FileState(TestPath(file)).LastWriteTimeUtcFast); } [Fact] @@ -383,7 +389,7 @@ public void DoesNotExistLength() { string file = Guid.NewGuid().ToString("N"); // presumably doesn't exist - Helpers.VerifyAssertThrowsSameWay(delegate () { var x = new FileInfo(file).Length; }, delegate () { var x = new FileState(file).Length; }); + Helpers.VerifyAssertThrowsSameWay(delegate () { var x = new FileInfo(file).Length; }, delegate () { var x = new FileState(TestPath(file)).Length; }); } [Fact] @@ -393,7 +399,7 @@ public void DoesNotExistIsDirectory() { string file = Guid.NewGuid().ToString("N"); // presumably doesn't exist - var x = new FileState(file).IsDirectory; + var x = new FileState(TestPath(file)).IsDirectory; }); } [Fact] @@ -401,7 +407,7 @@ public void DoesNotExistDirectoryOrFileExists() { string file = Guid.NewGuid().ToString("N"); // presumably doesn't exist - Assert.Equal(Directory.Exists(file), new FileState(file).DirectoryExists); + Assert.Equal(Directory.Exists(file), new FileState(TestPath(file)).DirectoryExists); } [Fact] @@ -409,8 +415,8 @@ public void DoesNotExistParentFolderNotFound() { string file = Guid.NewGuid().ToString("N") + "\\x"; // presumably doesn't exist - Assert.False(new FileState(file).FileExists); - Assert.False(new FileState(file).DirectoryExists); + Assert.False(new FileState(TestPath(file)).FileExists); + Assert.False(new FileState(TestPath(file)).DirectoryExists); } } } diff --git a/src/Tasks.UnitTests/MakeDir_Tests.cs b/src/Tasks.UnitTests/MakeDir_Tests.cs index 7b29328f258..c9cc65b0085 100644 --- a/src/Tasks.UnitTests/MakeDir_Tests.cs +++ b/src/Tasks.UnitTests/MakeDir_Tests.cs @@ -29,6 +29,7 @@ public void AttributeForwarding() MakeDir t = new MakeDir(); MockEngine engine = new MockEngine(); t.BuildEngine = engine; + t.TaskEnvironment = TaskEnvironmentHelper.CreateForTest(); t.Directories = new ITaskItem[] { @@ -76,6 +77,7 @@ public void SomeInputsFailToCreate() MakeDir t = new MakeDir(); MockEngine engine = new MockEngine(); t.BuildEngine = engine; + t.TaskEnvironment = TaskEnvironmentHelper.CreateForTest(); t.Directories = new ITaskItem[] { @@ -133,6 +135,7 @@ public void CreateNewDirectory() MakeDir t = new MakeDir(); MockEngine engine = new MockEngine(); t.BuildEngine = engine; + t.TaskEnvironment = TaskEnvironmentHelper.CreateForTest(); t.Directories = new ITaskItem[] { @@ -183,6 +186,7 @@ public void QuestionCreateNewDirectory() MakeDir t = new MakeDir(); MockEngine engine = new MockEngine(); t.BuildEngine = engine; + t.TaskEnvironment = TaskEnvironmentHelper.CreateForTest(); t.FailIfNotIncremental = true; t.Directories = dirList; @@ -199,6 +203,7 @@ public void QuestionCreateNewDirectory() engine.Log = ""; t = new MakeDir(); t.BuildEngine = engine; + t.TaskEnvironment = TaskEnvironmentHelper.CreateForTest(); t.Directories = dirList; success = t.Execute(); Assert.True(success); @@ -241,6 +246,7 @@ public void FileAlreadyExists() MakeDir t = new MakeDir(); MockEngine engine = new MockEngine(); t.BuildEngine = engine; + t.TaskEnvironment = TaskEnvironmentHelper.CreateForTest(); t.Directories = new ITaskItem[] { diff --git a/src/Tasks.UnitTests/ReadLinesFromFile_Tests.cs b/src/Tasks.UnitTests/ReadLinesFromFile_Tests.cs index 689c4f7f1aa..267cb7a756b 100644 --- a/src/Tasks.UnitTests/ReadLinesFromFile_Tests.cs +++ b/src/Tasks.UnitTests/ReadLinesFromFile_Tests.cs @@ -31,6 +31,7 @@ public void Basic() // Append one line to the file. var a = new WriteLinesToFile { + TaskEnvironment = TaskEnvironmentHelper.CreateForTest(), File = new TaskItem(file), Lines = new ITaskItem[] { new TaskItem("Line1") } }; @@ -39,6 +40,7 @@ public void Basic() // Read the line from the file. var r = new ReadLinesFromFile { + TaskEnvironment = TaskEnvironmentHelper.CreateForTest(), File = new TaskItem(file) }; Assert.True(r.Execute()); @@ -78,6 +80,7 @@ public void Escaping() // Append one line to the file. var a = new WriteLinesToFile { + TaskEnvironment = TaskEnvironmentHelper.CreateForTest(), File = new TaskItem(file), Lines = new ITaskItem[] { new TaskItem("Line1_%253b_") } }; @@ -86,6 +89,7 @@ public void Escaping() // Read the line from the file. var r = new ReadLinesFromFile { + TaskEnvironment = TaskEnvironmentHelper.CreateForTest(), File = new TaskItem(file) }; Assert.True(r.Execute()); @@ -123,6 +127,7 @@ public void ANSINonASCII() // Append one line to the file. var a = new WriteLinesToFile { + TaskEnvironment = TaskEnvironmentHelper.CreateForTest(), File = new TaskItem(file), Lines = new ITaskItem[] { new TaskItem("My special character is \u00C3") } }; @@ -131,6 +136,7 @@ public void ANSINonASCII() // Read the line from the file. var r = new ReadLinesFromFile { + TaskEnvironment = TaskEnvironmentHelper.CreateForTest(), File = new TaskItem(file) }; Assert.True(r.Execute()); @@ -155,7 +161,8 @@ public void ReadMissing() // Read the line from the file. var r = new ReadLinesFromFile { - File = new TaskItem(file) + TaskEnvironment = TaskEnvironmentHelper.CreateForTest(), + File = new TaskItem(file) }; Assert.True(r.Execute()); @@ -175,6 +182,7 @@ public void IgnoreBlankLines() // Append one line to the file. var a = new WriteLinesToFile { + TaskEnvironment = TaskEnvironmentHelper.CreateForTest(), File = new TaskItem(file), Lines = new ITaskItem[] { @@ -191,6 +199,7 @@ public void IgnoreBlankLines() // Read the line from the file. var r = new ReadLinesFromFile { + TaskEnvironment = TaskEnvironmentHelper.CreateForTest(), File = new TaskItem(file) }; Assert.True(r.Execute()); @@ -225,6 +234,7 @@ public void ReadNoAccess() // Append one line to the file. var a = new WriteLinesToFile { + TaskEnvironment = TaskEnvironmentHelper.CreateForTest(), File = new TaskItem(file), Lines = new ITaskItem[] { new TaskItem("This is a new line") } }; @@ -240,6 +250,7 @@ public void ReadNoAccess() var r = new ReadLinesFromFile(); var mEngine = new MockEngine(); r.BuildEngine = mEngine; + r.TaskEnvironment = TaskEnvironmentHelper.CreateForTest(); r.File = new TaskItem(file); Assert.False(r.Execute()); } diff --git a/src/Tasks.UnitTests/RemoveDir_Tests.cs b/src/Tasks.UnitTests/RemoveDir_Tests.cs index c0dd5b24cc2..5e01b3a91de 100644 --- a/src/Tasks.UnitTests/RemoveDir_Tests.cs +++ b/src/Tasks.UnitTests/RemoveDir_Tests.cs @@ -36,6 +36,7 @@ public void AttributeForwarding() i.SetMetadata("Locale", "en-GB"); t.Directories = new ITaskItem[] { i }; t.BuildEngine = new MockEngine(_output); + t.TaskEnvironment = TaskEnvironmentHelper.CreateForTest(); t.Execute(); @@ -61,6 +62,7 @@ public void SimpleDelete() { Directories = list.ToArray(), BuildEngine = new MockEngine(_output), + TaskEnvironment = TaskEnvironmentHelper.CreateForTest(), FailIfNotIncremental = true, }; t.Execute().ShouldBeFalse(); @@ -69,6 +71,7 @@ public void SimpleDelete() { Directories = list.ToArray(), BuildEngine = new MockEngine(_output), + TaskEnvironment = TaskEnvironmentHelper.CreateForTest(), }; t2.Execute().ShouldBeTrue(); t2.RemovedDirectories.Length.ShouldBe(list.Count); @@ -83,6 +86,7 @@ public void SimpleDelete() { Directories = list.ToArray(), BuildEngine = new MockEngine(_output), + TaskEnvironment = TaskEnvironmentHelper.CreateForTest(), FailIfNotIncremental = true, }; t3.Execute().ShouldBeTrue(); @@ -108,6 +112,7 @@ public void DeleteEmptyDirectory_WarnsAndContinues() RemoveDir t = new RemoveDir(); t.Directories = list.ToArray(); t.BuildEngine = new MockEngine(_output); + t.TaskEnvironment = TaskEnvironmentHelper.CreateForTest(); t.Execute().ShouldBeTrue(); t.RemovedDirectories.Length.ShouldBe(0); diff --git a/src/Tasks.UnitTests/Touch_Tests.cs b/src/Tasks.UnitTests/Touch_Tests.cs index 7f6da5b32ee..89a9a7c8165 100644 --- a/src/Tasks.UnitTests/Touch_Tests.cs +++ b/src/Tasks.UnitTests/Touch_Tests.cs @@ -187,6 +187,7 @@ public void TouchExisting() Touch t = new Touch(); MockEngine engine = new MockEngine(); t.BuildEngine = engine; + t.TaskEnvironment = TaskEnvironmentHelper.CreateForTest(); t.Files = new ITaskItem[] { @@ -210,6 +211,7 @@ public void TouchNonExisting() Touch t = new Touch(); MockEngine engine = new MockEngine(); t.BuildEngine = engine; + t.TaskEnvironment = TaskEnvironmentHelper.CreateForTest(); t.Files = new ITaskItem[] { @@ -232,6 +234,7 @@ public void TouchNonExistingAlwaysCreate() Touch t = new Touch(); MockEngine engine = new MockEngine(); t.BuildEngine = engine; + t.TaskEnvironment = TaskEnvironmentHelper.CreateForTest(); t.AlwaysCreate = true; t.Files = new ITaskItem[] @@ -255,6 +258,7 @@ public void TouchNonExistingAlwaysCreateAndBadlyFormedTimestamp() Touch t = new Touch(); MockEngine engine = new MockEngine(); t.BuildEngine = engine; + t.TaskEnvironment = TaskEnvironmentHelper.CreateForTest(); t.AlwaysCreate = true; t.ForceTouch = false; t.Time = "Badly formed time String."; @@ -278,6 +282,7 @@ public void TouchReadonly() Touch t = new Touch(); MockEngine engine = new MockEngine(); t.BuildEngine = engine; + t.TaskEnvironment = TaskEnvironmentHelper.CreateForTest(); t.AlwaysCreate = true; t.Files = new ITaskItem[] @@ -300,6 +305,7 @@ public void TouchReadonlyForce() Touch t = new Touch(); MockEngine engine = new MockEngine(); t.BuildEngine = engine; + t.TaskEnvironment = TaskEnvironmentHelper.CreateForTest(); t.ForceTouch = true; t.AlwaysCreate = true; @@ -317,6 +323,7 @@ public void TouchNonExistingDirectoryDoesntExist() Touch t = new Touch(); MockEngine engine = new MockEngine(); t.BuildEngine = engine; + t.TaskEnvironment = TaskEnvironmentHelper.CreateForTest(); t.AlwaysCreate = true; t.Files = new ITaskItem[] @@ -342,6 +349,7 @@ public void QuestionTouchNonExisting() Touch t = new Touch(); MockEngine engine = new MockEngine(); t.BuildEngine = engine; + t.TaskEnvironment = TaskEnvironmentHelper.CreateForTest(); t.FailIfNotIncremental = true; t.Files = new ITaskItem[] @@ -368,6 +376,7 @@ public void QuestionTouchNonExistingAlwaysCreate() Touch t = new Touch(); MockEngine engine = new MockEngine(); t.BuildEngine = engine; + t.TaskEnvironment = TaskEnvironmentHelper.CreateForTest(); t.FailIfNotIncremental = true; t.AlwaysCreate = true; t.Files = new ITaskItem[] @@ -393,6 +402,7 @@ public void QuestionTouchExisting() Touch t = new Touch(); MockEngine engine = new MockEngine(); t.BuildEngine = engine; + t.TaskEnvironment = TaskEnvironmentHelper.CreateForTest(); t.FailIfNotIncremental = true; t.Files = new ITaskItem[] { diff --git a/src/Tasks.UnitTests/WriteLinesToFile_Tests.cs b/src/Tasks.UnitTests/WriteLinesToFile_Tests.cs index 48ee8f14cce..a712746c098 100644 --- a/src/Tasks.UnitTests/WriteLinesToFile_Tests.cs +++ b/src/Tasks.UnitTests/WriteLinesToFile_Tests.cs @@ -38,6 +38,7 @@ public void InvalidEncoding() var a = new WriteLinesToFile { BuildEngine = new MockEngine(_output), + TaskEnvironment = TaskEnvironmentHelper.CreateForTest(), Encoding = "||invalid||", File = new TaskItem("c:\\" + Guid.NewGuid().ToString()), Lines = new TaskItem[] { new TaskItem("x") } @@ -61,6 +62,7 @@ public void Encoding() var a = new WriteLinesToFile { BuildEngine = new MockEngine(_output), + TaskEnvironment = TaskEnvironmentHelper.CreateForTest(), File = new TaskItem(file), Lines = new ITaskItem[] { new TaskItem("\uBDEA") } }; @@ -68,7 +70,8 @@ public void Encoding() var r = new ReadLinesFromFile { - File = new TaskItem(file) + File = new TaskItem(file), + TaskEnvironment = TaskEnvironmentHelper.CreateForTest() }; Assert.True(r.Execute()); @@ -80,6 +83,7 @@ public void Encoding() a = new WriteLinesToFile { BuildEngine = new MockEngine(_output), + TaskEnvironment = TaskEnvironmentHelper.CreateForTest(), File = new TaskItem(file), Lines = new ITaskItem[] { new TaskItem("\uBDEA") }, Encoding = "ASCII" @@ -89,7 +93,8 @@ public void Encoding() // Read the line from the file. r = new ReadLinesFromFile { - File = new TaskItem(file) + File = new TaskItem(file), + TaskEnvironment = TaskEnvironmentHelper.CreateForTest() }; Assert.True(r.Execute()); @@ -112,6 +117,7 @@ public void WriteLinesWriteOnlyWhenDifferentTest() { Overwrite = true, BuildEngine = new MockEngine(_output), + TaskEnvironment = TaskEnvironmentHelper.CreateForTest(), File = new TaskItem(file), WriteOnlyWhenDifferent = true, Lines = new ITaskItem[] { new TaskItem("File contents1") } @@ -120,7 +126,7 @@ public void WriteLinesWriteOnlyWhenDifferentTest() a.Execute().ShouldBeTrue(); // Verify contents - var r = new ReadLinesFromFile { File = new TaskItem(file) }; + var r = new ReadLinesFromFile { File = new TaskItem(file), TaskEnvironment = TaskEnvironmentHelper.CreateForTest() }; r.Execute().ShouldBeTrue(); r.Lines[0].ItemSpec.ShouldBe("File contents1"); @@ -133,6 +139,7 @@ public void WriteLinesWriteOnlyWhenDifferentTest() { Overwrite = true, BuildEngine = new MockEngine(_output), + TaskEnvironment = TaskEnvironmentHelper.CreateForTest(), File = new TaskItem(file), WriteOnlyWhenDifferent = true, Lines = new ITaskItem[] { new TaskItem("File contents1") } @@ -145,6 +152,7 @@ public void WriteLinesWriteOnlyWhenDifferentTest() { Overwrite = true, BuildEngine = new MockEngine(_output), + TaskEnvironment = TaskEnvironmentHelper.CreateForTest(), File = new TaskItem(file), WriteOnlyWhenDifferent = true, Lines = new ITaskItem[] { new TaskItem("File contents2") } @@ -171,6 +179,7 @@ public void RedundantParametersAreLogged() WriteLinesToFile task = new() { BuildEngine = engine, + TaskEnvironment = TaskEnvironmentHelper.CreateForTest(), File = new TaskItem(file), Lines = new ITaskItem[] { new TaskItem($"{nameof(RedundantParametersAreLogged)} Test") }, WriteOnlyWhenDifferent = true, @@ -195,6 +204,7 @@ public void QuestionWriteLinesWriteOnlyWhenDifferentTest() { Overwrite = true, BuildEngine = new MockEngine(_output), + TaskEnvironment = TaskEnvironmentHelper.CreateForTest(), File = new TaskItem(file), WriteOnlyWhenDifferent = true, Lines = new ITaskItem[] { new TaskItem("File contents1") } @@ -203,7 +213,7 @@ public void QuestionWriteLinesWriteOnlyWhenDifferentTest() a.Execute().ShouldBeTrue(); // Verify contents - var r = new ReadLinesFromFile { File = new TaskItem(file) }; + var r = new ReadLinesFromFile { File = new TaskItem(file), TaskEnvironment = TaskEnvironmentHelper.CreateForTest() }; r.Execute().ShouldBeTrue(); r.Lines[0].ItemSpec.ShouldBe("File contents1"); @@ -216,6 +226,7 @@ public void QuestionWriteLinesWriteOnlyWhenDifferentTest() { Overwrite = true, BuildEngine = new MockEngine(_output), + TaskEnvironment = TaskEnvironmentHelper.CreateForTest(), File = new TaskItem(file), WriteOnlyWhenDifferent = true, Lines = new ITaskItem[] { new TaskItem("File contents1") }, @@ -229,6 +240,7 @@ public void QuestionWriteLinesWriteOnlyWhenDifferentTest() { Overwrite = true, BuildEngine = new MockEngine(_output), + TaskEnvironment = TaskEnvironmentHelper.CreateForTest(), File = new TaskItem(file), WriteOnlyWhenDifferent = true, Lines = new ITaskItem[] { new TaskItem("File contents2") }, @@ -274,6 +286,7 @@ void TestWriteLines(string fileExists, string fileNotExists, bool Overwrite, boo { Overwrite = Overwrite, BuildEngine = new MockEngine(_output), + TaskEnvironment = TaskEnvironmentHelper.CreateForTest(), File = new TaskItem(fileExists), WriteOnlyWhenDifferent = WriteOnlyWhenDifferent, FailIfNotIncremental = true, @@ -285,6 +298,7 @@ void TestWriteLines(string fileExists, string fileNotExists, bool Overwrite, boo { Overwrite = Overwrite, BuildEngine = new MockEngine(_output), + TaskEnvironment = TaskEnvironmentHelper.CreateForTest(), File = new TaskItem(fileNotExists), WriteOnlyWhenDifferent = WriteOnlyWhenDifferent, FailIfNotIncremental = true, @@ -308,6 +322,7 @@ public void WriteLinesToFileDoesCreateDirectory() var WriteLinesToFile = new WriteLinesToFile { BuildEngine = new MockEngine(_output), + TaskEnvironment = TaskEnvironmentHelper.CreateForTest(), File = new TaskItem(file), Lines = new ITaskItem[] { new TaskItem("WriteLinesToFileDoesCreateDirectory Test") } }; @@ -339,6 +354,7 @@ public void WritingNothingErasesExistingFile(bool useNullLines) { Overwrite = true, BuildEngine = new MockEngine(_output), + TaskEnvironment = TaskEnvironmentHelper.CreateForTest(), File = new TaskItem(file.Path), Lines = lines }.Execute().ShouldBeTrue(); @@ -365,6 +381,7 @@ public void WritingNothingCreatesNewFile(bool useNullLines) { Overwrite = true, BuildEngine = new MockEngine(_output), + TaskEnvironment = TaskEnvironmentHelper.CreateForTest(), File = new TaskItem(file.Path), Lines = lines }.Execute().ShouldBeTrue(); diff --git a/src/Tasks/Copy.cs b/src/Tasks/Copy.cs index 208bb6ca298..a3adcf74260 100644 --- a/src/Tasks/Copy.cs +++ b/src/Tasks/Copy.cs @@ -21,7 +21,8 @@ namespace Microsoft.Build.Tasks /// /// A task that copies files. /// - public class Copy : TaskExtension, IIncrementalTask, ICancelableTask + [MSBuildMultiThreadableTask] + public class Copy : TaskExtension, IIncrementalTask, ICancelableTask, IMultiThreadableTask { internal const string AlwaysRetryEnvVar = "MSBUILDALWAYSRETRY"; internal const string AlwaysOverwriteReadOnlyFilesEnvVar = "MSBUILDALWAYSOVERWRITEREADONLYFILES"; @@ -185,6 +186,9 @@ public Copy() public bool FailIfNotIncremental { get; set; } + /// + public TaskEnvironment TaskEnvironment { get; set; } + #endregion /// @@ -260,7 +264,7 @@ private void LogAlwaysRetryDiagnosticFromResources(string messageResourceName, p { if (destinationFileState.DirectoryExists) { - Log.LogErrorWithCodeFromResources("Copy.DestinationIsDirectory", sourceFileState.Name, destinationFileState.Name); + Log.LogErrorWithCodeFromResources("Copy.DestinationIsDirectory", sourceFileState.Path.OriginalValue, destinationFileState.Path.OriginalValue); return false; } @@ -270,17 +274,18 @@ private void LogAlwaysRetryDiagnosticFromResources(string messageResourceName, p // error telling the user so. Otherwise, .NET Framework's File.Copy method will throw // an UnauthorizedAccessException saying "access is denied", which is not very useful // to the user. - Log.LogErrorWithCodeFromResources("Copy.SourceIsDirectory", sourceFileState.Name); + Log.LogErrorWithCodeFromResources("Copy.SourceIsDirectory", sourceFileState.Path.OriginalValue); return false; } if (!sourceFileState.FileExists) { - Log.LogErrorWithCodeFromResources("Copy.SourceFileNotFound", sourceFileState.Name); + Log.LogErrorWithCodeFromResources("Copy.SourceFileNotFound", sourceFileState.Path.OriginalValue); return false; } - string destinationFolder = Path.GetDirectoryName(destinationFileState.Name); + string destinationFolder = Path.GetDirectoryName(destinationFileState.Path); + string originalDestinationFolder = Path.GetDirectoryName(destinationFileState.Path.OriginalValue); if (!string.IsNullOrEmpty(destinationFolder) && !_directoriesKnownToExist.ContainsKey(destinationFolder)) { @@ -288,12 +293,12 @@ private void LogAlwaysRetryDiagnosticFromResources(string messageResourceName, p { if (FailIfNotIncremental) { - Log.LogError(CreatesDirectory, destinationFolder); + Log.LogError(CreatesDirectory, originalDestinationFolder); return false; } else { - Log.LogMessage(MessageImportance.Normal, CreatesDirectory, destinationFolder); + Log.LogMessage(MessageImportance.Normal, CreatesDirectory, originalDestinationFolder); Directory.CreateDirectory(destinationFolder); } } @@ -306,7 +311,8 @@ private void LogAlwaysRetryDiagnosticFromResources(string messageResourceName, p if (FailIfNotIncremental) { - Log.LogError(FileComment, sourceFileState.FileNameFullPath, destinationFileState.FileNameFullPath); + // Before the introduction of AbsolutePath, this logged full paths, so preserve that behavior + Log.LogError(FileComment, sourceFileState.Path, destinationFileState.Path); return false; } @@ -319,7 +325,7 @@ private void LogAlwaysRetryDiagnosticFromResources(string messageResourceName, p destinationFileState.FileExists && !destinationFileState.IsReadOnly) { - FileUtilities.DeleteNoThrow(destinationFileState.Name); + FileUtilities.DeleteNoThrow(destinationFileState.Path); } bool symbolicLinkCreated = false; @@ -335,11 +341,12 @@ private void LogAlwaysRetryDiagnosticFromResources(string messageResourceName, p if (UseSymboliclinksIfPossible) { // This is a message for fallback to SymbolicLinks if HardLinks fail when UseHardlinksIfPossible and UseSymboliclinksIfPossible are true - Log.LogMessage(MessageImportance.Normal, RetryingAsSymbolicLink, sourceFileState.FileNameFullPath, destinationFileState.FileNameFullPath, errorMessage); + // Before the introduction of AbsolutePath, this logged full paths, so preserve that behavior + Log.LogMessage(MessageImportance.Normal, RetryingAsSymbolicLink, sourceFileState.Path, destinationFileState.Path, errorMessage); } else { - Log.LogMessage(MessageImportance.Normal, RetryingAsFileCopy, sourceFileState.FileNameFullPath, destinationFileState.FileNameFullPath, errorMessage); + Log.LogMessage(MessageImportance.Normal, RetryingAsFileCopy, sourceFileState.Path, destinationFileState.Path, errorMessage); } } } @@ -355,13 +362,13 @@ private void LogAlwaysRetryDiagnosticFromResources(string messageResourceName, p errorMessage = Log.FormatResourceString("Copy.NonWindowsLinkErrorMessage", "symlink()", errorMessage); } - Log.LogMessage(MessageImportance.Normal, RetryingAsFileCopy, sourceFileState.FileNameFullPath, destinationFileState.FileNameFullPath, errorMessage); + Log.LogMessage(MessageImportance.Normal, RetryingAsFileCopy, sourceFileState.Path, destinationFileState.Path, errorMessage); } } if (ErrorIfLinkFails && !hardLinkCreated && !symbolicLinkCreated) { - Log.LogErrorWithCodeFromResources("Copy.LinkFailed", sourceFileState.FileNameFullPath, destinationFileState.FileNameFullPath); + Log.LogErrorWithCodeFromResources("Copy.LinkFailed", sourceFileState.Path, destinationFileState.Path); return false; } @@ -370,9 +377,9 @@ private void LogAlwaysRetryDiagnosticFromResources(string messageResourceName, p if (!hardLinkCreated && !symbolicLinkCreated) { // Do not log a fake command line as well, as it's superfluous, and also potentially expensive - Log.LogMessage(MessageImportance.Normal, FileComment, sourceFileState.FileNameFullPath, destinationFileState.FileNameFullPath); + Log.LogMessage(MessageImportance.Normal, FileComment, sourceFileState.Path, destinationFileState.Path); - File.Copy(sourceFileState.Name, destinationFileState.Name, true); + File.Copy(sourceFileState.Path, destinationFileState.Path, true); } // If the destinationFile file exists, then make sure it's read-write. @@ -393,9 +400,9 @@ private void LogAlwaysRetryDiagnosticFromResources(string messageResourceName, p private void TryCopyViaLink(string linkComment, MessageImportance messageImportance, FileState sourceFileState, FileState destinationFileState, out bool linkCreated, ref string errorMessage, Func createLink) { // Do not log a fake command line as well, as it's superfluous, and also potentially expensive - Log.LogMessage(MessageImportance.Normal, linkComment, sourceFileState.FileNameFullPath, destinationFileState.FileNameFullPath); + Log.LogMessage(MessageImportance.Normal, linkComment, sourceFileState.Path, destinationFileState.Path); - linkCreated = createLink(sourceFileState.Name, destinationFileState.Name, errorMessage); + linkCreated = createLink(sourceFileState.Path, destinationFileState.Path, errorMessage); } /// @@ -410,10 +417,10 @@ private void MakeFileWriteable(FileState file, bool logActivity) { if (logActivity) { - Log.LogMessage(MessageImportance.Low, RemovingReadOnlyAttribute, file.Name); + Log.LogMessage(MessageImportance.Low, RemovingReadOnlyAttribute, file.Path.OriginalValue); } - File.SetAttributes(file.Name, FileAttributes.Normal); + File.SetAttributes(file.Path, FileAttributes.Normal); file.Reset(); } } @@ -444,7 +451,7 @@ internal bool Execute( } // Environment variable stomps on user-requested value if it's set. - if (Environment.GetEnvironmentVariable(AlwaysOverwriteReadOnlyFilesEnvVar) != null) + if (TaskEnvironment.GetEnvironmentVariable(AlwaysOverwriteReadOnlyFilesEnvVar) != null) { OverwriteReadOnlyFiles = true; } @@ -497,11 +504,17 @@ private bool CopySingleThreaded( for (int i = 0; i < SourceFiles.Length && !_cancellationTokenSource.IsCancellationRequested; ++i) { bool copyComplete = false; - string destPath = DestinationFiles[i].ItemSpec; - MSBuildEventSource.Log.CopyUpToDateStart(destPath); - if (filesActuallyCopied.TryGetValue(destPath, out string originalSource)) + string sourceSpec = SourceFiles[i].ItemSpec; + string destSpec = DestinationFiles[i].ItemSpec; + + // Compute absolute paths once - reused for ETW, deduplication dictionary, and FileState + AbsolutePath sourceAbsolutePath = TaskEnvironment.GetAbsolutePath(sourceSpec); + AbsolutePath destAbsolutePath = TaskEnvironment.GetAbsolutePath(destSpec); + + MSBuildEventSource.Log.CopyUpToDateStart(destAbsolutePath); + if (filesActuallyCopied.TryGetValue(destAbsolutePath, out string originalSource)) { - if (String.Equals(originalSource, SourceFiles[i].ItemSpec, FileUtilities.PathComparison)) + if (originalSource == sourceAbsolutePath) { // Already copied from this location, don't copy again. copyComplete = true; @@ -510,9 +523,9 @@ private bool CopySingleThreaded( if (!copyComplete) { - if (DoCopyIfNecessary(new FileState(SourceFiles[i].ItemSpec), new FileState(DestinationFiles[i].ItemSpec), copyFile)) + if (DoCopyIfNecessary(new FileState(sourceAbsolutePath), new FileState(destAbsolutePath), copyFile)) { - filesActuallyCopied[destPath] = SourceFiles[i].ItemSpec; + filesActuallyCopied[destAbsolutePath] = sourceAbsolutePath; copyComplete = true; } else @@ -522,7 +535,7 @@ private bool CopySingleThreaded( } else { - MSBuildEventSource.Log.CopyUpToDateStop(destPath, true); + MSBuildEventSource.Log.CopyUpToDateStop(destAbsolutePath.OriginalValue, true); } if (copyComplete) @@ -638,26 +651,35 @@ void ProcessPartition() { while (partitionQueue.TryDequeue(out List partition)) { + // Cache the previous source absolute path to avoid recomputing it + AbsolutePath prevSourceAbsolutePath = default; + for (int partitionIndex = 0; partitionIndex < partition.Count && !_cancellationTokenSource.IsCancellationRequested; partitionIndex++) { int fileIndex = partition[partitionIndex]; ITaskItem sourceItem = SourceFiles[fileIndex]; ITaskItem destItem = DestinationFiles[fileIndex]; - string sourcePath = sourceItem.ItemSpec; + string sourceSpec = sourceItem.ItemSpec; + string destSpec = destItem.ItemSpec; + // Compute absolute paths once - reused for ETW, deduplication check, and FileState + AbsolutePath sourceAbsolutePath = TaskEnvironment.GetAbsolutePath(sourceSpec); + AbsolutePath destAbsolutePath = TaskEnvironment.GetAbsolutePath(destSpec); + // Check if we just copied from this location to the destination, don't copy again. - MSBuildEventSource.Log.CopyUpToDateStart(destItem.ItemSpec); - bool copyComplete = partitionIndex > 0 && - String.Equals( - sourcePath, - SourceFiles[partition[partitionIndex - 1]].ItemSpec, - FileUtilities.PathComparison); + MSBuildEventSource.Log.CopyUpToDateStart(destAbsolutePath); + bool copyComplete = false; + if (partitionIndex > 0) + { + // Use cached absolute path from previous iteration instead of recomputing + copyComplete = sourceAbsolutePath == prevSourceAbsolutePath; + } if (!copyComplete) { if (DoCopyIfNecessary( - new FileState(sourceItem.ItemSpec), - new FileState(destItem.ItemSpec), + new FileState(sourceAbsolutePath), + new FileState(destAbsolutePath), copyFile)) { copyComplete = true; @@ -670,7 +692,7 @@ void ProcessPartition() } else { - MSBuildEventSource.Log.CopyUpToDateStop(destItem.ItemSpec, true); + MSBuildEventSource.Log.CopyUpToDateStop(destAbsolutePath.OriginalValue, true); } if (copyComplete) @@ -678,6 +700,9 @@ void ProcessPartition() sourceItem.CopyMetadataTo(destItem); successFlags[fileIndex] = (IntPtr)1; } + + // Cache for next iteration's duplicate check + prevSourceAbsolutePath = sourceAbsolutePath; } } } @@ -799,7 +824,8 @@ private bool InitializeDestinationFiles() foreach (ITaskItem sourceFolder in SourceFolders) { - string src = FileUtilities.NormalizePath(sourceFolder.ItemSpec); + ErrorUtilities.VerifyThrowArgumentLength(sourceFolder.ItemSpec); + AbsolutePath src = FrameworkFileUtilities.NormalizePath(TaskEnvironment.GetAbsolutePath(sourceFolder.ItemSpec)); string srcName = Path.GetFileName(src); (string[] filesInFolder, _, _, string globFailure) = FileMatcher.Default.GetFiles(src, "**"); @@ -900,19 +926,18 @@ private bool DoCopyIfNecessary(FileState sourceFileState, FileState destinationF Log.LogMessage( MessageImportance.Low, DidNotCopyBecauseOfFileMatch, - sourceFileState.Name, - destinationFileState.Name, + sourceFileState.Path.OriginalValue, + destinationFileState.Path.OriginalValue, "SkipUnchangedFiles", "true"); - MSBuildEventSource.Log.CopyUpToDateStop(destinationFileState.Name, true); + MSBuildEventSource.Log.CopyUpToDateStop(destinationFileState.Path.OriginalValue, true); } else if (!PathsAreIdentical(sourceFileState, destinationFileState)) { - MSBuildEventSource.Log.CopyUpToDateStop(destinationFileState.Name, false); - + MSBuildEventSource.Log.CopyUpToDateStop(destinationFileState.Path.OriginalValue, false); if (FailIfNotIncremental) { - Log.LogError(FileComment, sourceFileState.Name, destinationFileState.Name); + Log.LogError(FileComment, sourceFileState.Path.OriginalValue, destinationFileState.Path.OriginalValue); success = false; } else @@ -922,7 +947,7 @@ private bool DoCopyIfNecessary(FileState sourceFileState, FileState destinationF } else { - MSBuildEventSource.Log.CopyUpToDateStop(destinationFileState.Name, true); + MSBuildEventSource.Log.CopyUpToDateStop(destinationFileState.Path.OriginalValue, true); } } catch (OperationCanceledException) @@ -931,12 +956,12 @@ private bool DoCopyIfNecessary(FileState sourceFileState, FileState destinationF } catch (PathTooLongException e) { - Log.LogErrorWithCodeFromResources("Copy.Error", sourceFileState.Name, destinationFileState.Name, e.Message); + Log.LogErrorWithCodeFromResources("Copy.Error", sourceFileState.Path.OriginalValue, destinationFileState.Path.OriginalValue, e.Message); success = false; } catch (Exception e) when (ExceptionHandling.IsIoRelatedException(e)) { - Log.LogErrorWithCodeFromResources("Copy.Error", sourceFileState.Name, destinationFileState.Name, e.Message); + Log.LogErrorWithCodeFromResources("Copy.Error", sourceFileState.Path.OriginalValue, destinationFileState.Path.OriginalValue, e.Message); success = false; } @@ -976,7 +1001,7 @@ private bool DoCopyWithRetries(FileState sourceFileState, FileState destinationF case IOException: // Not clear why we can get one and not the other int code = Marshal.GetHRForException(e); - LogAlwaysRetryDiagnosticFromResources("Copy.IOException", e.ToString(), sourceFileState.Name, destinationFileState.Name, code); + LogAlwaysRetryDiagnosticFromResources("Copy.IOException", e.ToString(), sourceFileState.Path.OriginalValue, destinationFileState.Path.OriginalValue, code); if (code == NativeMethods.ERROR_ACCESS_DENIED) { // ERROR_ACCESS_DENIED can either mean there's an ACL preventing us, or the file has the readonly bit set. @@ -1006,7 +1031,7 @@ private bool DoCopyWithRetries(FileState sourceFileState, FileState destinationF break; } - if (DestinationFolder != null && FileSystems.Default.FileExists(DestinationFolder.ItemSpec)) + if (DestinationFolder != null && FileSystems.Default.FileExists(TaskEnvironment.GetAbsolutePath(DestinationFolder.ItemSpec))) { // We failed to create the DestinationFolder because it's an existing file. No sense retrying. // We don't check for this case upstream because it'd be another hit to the filesystem. @@ -1019,9 +1044,9 @@ private bool DoCopyWithRetries(FileState sourceFileState, FileState destinationF if (retries < Retries) { retries++; - Log.LogWarningWithCodeFromResources("Copy.Retrying", sourceFileState.Name, - destinationFileState.Name, retries, RetryDelayMilliseconds, e.Message, - LockCheck.GetLockedFileMessage(destinationFileState.Name)); + Log.LogWarningWithCodeFromResources("Copy.Retrying", sourceFileState.Path.OriginalValue, + destinationFileState.Path.OriginalValue, retries, RetryDelayMilliseconds, e.Message, + LockCheck.GetLockedFileMessage(destinationFileState.Path)); // if we have to retry for some reason, wipe the state -- it may not be correct anymore. destinationFileState.Reset(); @@ -1032,8 +1057,8 @@ private bool DoCopyWithRetries(FileState sourceFileState, FileState destinationF else if (Retries > 0) { // Exception message is logged in caller - Log.LogErrorWithCodeFromResources("Copy.ExceededRetries", sourceFileState.Name, - destinationFileState.Name, Retries, LockCheck.GetLockedFileMessage(destinationFileState.Name)); + Log.LogErrorWithCodeFromResources("Copy.ExceededRetries", sourceFileState.Path.OriginalValue, + destinationFileState.Path.OriginalValue, Retries, LockCheck.GetLockedFileMessage(destinationFileState.Path)); throw; } else @@ -1045,9 +1070,9 @@ private bool DoCopyWithRetries(FileState sourceFileState, FileState destinationF if (retries < Retries) { retries++; - Log.LogWarningWithCodeFromResources("Copy.Retrying", sourceFileState.Name, - destinationFileState.Name, retries, RetryDelayMilliseconds, String.Empty /* no details */, - LockCheck.GetLockedFileMessage(destinationFileState.Name)); + Log.LogWarningWithCodeFromResources("Copy.Retrying", sourceFileState.Path.OriginalValue, + destinationFileState.Path.OriginalValue, retries, RetryDelayMilliseconds, String.Empty /* no details */, + LockCheck.GetLockedFileMessage(destinationFileState.Path)); // if we have to retry for some reason, wipe the state -- it may not be correct anymore. destinationFileState.Reset(); @@ -1056,8 +1081,8 @@ private bool DoCopyWithRetries(FileState sourceFileState, FileState destinationF } else if (Retries > 0) { - Log.LogErrorWithCodeFromResources("Copy.ExceededRetries", sourceFileState.Name, - destinationFileState.Name, Retries, LockCheck.GetLockedFileMessage(destinationFileState.Name)); + Log.LogErrorWithCodeFromResources("Copy.ExceededRetries", sourceFileState.Path.OriginalValue, + destinationFileState.Path.OriginalValue, Retries, LockCheck.GetLockedFileMessage(destinationFileState.Path)); return false; } else @@ -1085,16 +1110,16 @@ public override bool Execute() /// Compares two paths to see if they refer to the same file. We can't solve the general /// canonicalization problem, so we just compare strings on the full paths. /// + /// + /// This method has a side effect of removing relative segments from the paths before comparison to avoid + /// false negatives due to different path representations. This operation may throw in certain cases (e.g. invalid paths on Windows). + /// TODO: refactor this task not to rely on this side effect for correct exception handling and caching + /// private static bool PathsAreIdentical(FileState source, FileState destination) { - if (string.Equals(source.Name, destination.Name, FileUtilities.PathComparison)) - { - return true; - } - - source.FileNameFullPath = Path.GetFullPath(source.Name); - destination.FileNameFullPath = Path.GetFullPath(destination.Name); - return string.Equals(source.FileNameFullPath, destination.FileNameFullPath, FileUtilities.PathComparison); + source.Path = FrameworkFileUtilities.RemoveRelativeSegments(source.Path); + destination.Path = FrameworkFileUtilities.RemoveRelativeSegments(destination.Path); + return source.Path == destination.Path; } private static bool GetParallelismFromEnvironment() diff --git a/src/Tasks/Delete.cs b/src/Tasks/Delete.cs index e10ad4f733f..d687b9c0006 100644 --- a/src/Tasks/Delete.cs +++ b/src/Tasks/Delete.cs @@ -17,7 +17,8 @@ namespace Microsoft.Build.Tasks /// /// Delete files from disk. /// - public class Delete : TaskExtension, ICancelableTask, IIncrementalTask + [MSBuildMultiThreadableTask] + public class Delete : TaskExtension, ICancelableTask, IIncrementalTask, IMultiThreadableTask { #region Properties @@ -63,6 +64,9 @@ public ITaskItem[] Files /// public bool FailIfNotIncremental { get; set; } + /// + public TaskEnvironment TaskEnvironment { get; set; } + /// /// Verify that the inputs are correct. /// @@ -115,27 +119,30 @@ public override bool Execute() } int retries = 0; + // deletedFilesSet is not normalized to save time on allocation while (!deletedFilesSet.Contains(file.ItemSpec)) { + AbsolutePath? filePath = null; try { - if (FileSystems.Default.FileExists(file.ItemSpec)) + filePath = TaskEnvironment.GetAbsolutePath(file.ItemSpec); + if (FileSystems.Default.FileExists(filePath)) { if (FailIfNotIncremental) { - Log.LogWarningFromResources("Delete.DeletingFile", file.ItemSpec); + Log.LogWarningFromResources("Delete.DeletingFile", filePath.Value.OriginalValue); } else { // Do not log a fake command line as well, as it's superfluous, and also potentially expensive - Log.LogMessageFromResources(MessageImportance.Normal, "Delete.DeletingFile", file.ItemSpec); + Log.LogMessageFromResources(MessageImportance.Normal, "Delete.DeletingFile", filePath.Value.OriginalValue); } - File.Delete(file.ItemSpec); + File.Delete(filePath); } else { - Log.LogMessageFromResources(MessageImportance.Low, "Delete.SkippingNonexistentFile", file.ItemSpec); + Log.LogMessageFromResources(MessageImportance.Low, "Delete.SkippingNonexistentFile", filePath.Value.OriginalValue); } // keep a running list of the files that were actually deleted // note that we include in this list files that did not exist @@ -146,18 +153,25 @@ public override bool Execute() } catch (Exception e) when (ExceptionHandling.IsIoRelatedException(e)) { - string lockedFileMessage = LockCheck.GetLockedFileMessage(file?.ItemSpec ?? string.Empty); + string lockedFileMessage = LockCheck.GetLockedFileMessage(filePath); if (retries < Retries) { retries++; - Log.LogWarningWithCodeFromResources("Delete.Retrying", file.ToString(), retries, RetryDelayMilliseconds, e.Message, lockedFileMessage); + Log.LogWarningWithCodeFromResources("Delete.Retrying", filePath?.OriginalValue ?? file.ItemSpec, retries, RetryDelayMilliseconds, e.Message, lockedFileMessage); Thread.Sleep(RetryDelayMilliseconds); continue; } else { - LogError(file, e, lockedFileMessage); + if (TreatErrorsAsWarnings) + { + Log.LogWarningWithCodeFromResources("Delete.Error", filePath?.OriginalValue ?? file.ItemSpec, e.Message, lockedFileMessage); + } + else + { + Log.LogErrorWithCodeFromResources("Delete.Error", filePath?.OriginalValue ?? file.ItemSpec, e.Message, lockedFileMessage); + } // Add on failure to avoid reattempting deletedFilesSet.Add(file.ItemSpec); } @@ -168,25 +182,6 @@ public override bool Execute() DeletedFiles = deletedFilesList.ToArray(); return !Log.HasLoggedErrors; } - - /// - /// Log an error. - /// - /// The file that wasn't deleted. - /// The exception. - /// Message from . - private void LogError(ITaskItem file, Exception e, string lockedFileMessage) - { - if (TreatErrorsAsWarnings) - { - Log.LogWarningWithCodeFromResources("Delete.Error", file.ItemSpec, e.Message, lockedFileMessage); - } - else - { - Log.LogErrorWithCodeFromResources("Delete.Error", file.ItemSpec, e.Message, lockedFileMessage); - } - } - #endregion } } diff --git a/src/Tasks/FileIO/ReadLinesFromFile.cs b/src/Tasks/FileIO/ReadLinesFromFile.cs index 86b5ca17068..1b80db354aa 100644 --- a/src/Tasks/FileIO/ReadLinesFromFile.cs +++ b/src/Tasks/FileIO/ReadLinesFromFile.cs @@ -15,7 +15,8 @@ namespace Microsoft.Build.Tasks /// /// Read a list of items from a file. /// - public class ReadLinesFromFile : TaskExtension + [MSBuildMultiThreadableTask] + public class ReadLinesFromFile : TaskExtension, IMultiThreadableTask { /// /// File to read lines from. @@ -23,6 +24,9 @@ public class ReadLinesFromFile : TaskExtension [Required] public ITaskItem File { get; set; } + /// + public TaskEnvironment TaskEnvironment { get; set; } + /// /// Receives lines from file. /// @@ -38,12 +42,12 @@ public override bool Execute() bool success = true; if (File != null) { - if (FileSystems.Default.FileExists(File.ItemSpec)) + AbsolutePath filePath = TaskEnvironment.GetAbsolutePath(File.ItemSpec); + if (FileSystems.Default.FileExists(filePath)) { try { - string[] textLines = System.IO.File.ReadAllLines(File.ItemSpec); - + string[] textLines = System.IO.File.ReadAllLines(filePath); var nonEmptyLines = new List(); char[] charsToTrim = { '\0', ' ', '\t' }; @@ -66,7 +70,7 @@ public override bool Execute() } catch (Exception e) when (ExceptionHandling.IsIoRelatedException(e)) { - Log.LogErrorWithCodeFromResources("ReadLinesFromFile.ErrorOrWarning", File.ItemSpec, e.Message); + Log.LogErrorWithCodeFromResources("ReadLinesFromFile.ErrorOrWarning", filePath.OriginalValue, e.Message); success = false; } } diff --git a/src/Tasks/FileIO/WriteLinesToFile.cs b/src/Tasks/FileIO/WriteLinesToFile.cs index 15b93fc25f0..560264ad909 100644 --- a/src/Tasks/FileIO/WriteLinesToFile.cs +++ b/src/Tasks/FileIO/WriteLinesToFile.cs @@ -16,11 +16,15 @@ namespace Microsoft.Build.Tasks /// /// Appends a list of items to a file. One item per line with carriage returns in-between. /// - public class WriteLinesToFile : TaskExtension, IIncrementalTask + [MSBuildMultiThreadableTask] + public class WriteLinesToFile : TaskExtension, IIncrementalTask, IMultiThreadableTask { // Default encoding taken from System.IO.WriteAllText() private static readonly Encoding s_defaultEncoding = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false, throwOnInvalidBytes: true); + /// + public TaskEnvironment TaskEnvironment { get; set; } + /// /// File to write lines to. /// @@ -62,15 +66,13 @@ public class WriteLinesToFile : TaskExtension, IIncrementalTask /// public override bool Execute() { - bool success = true; - if (File == null) { - return success; + return true; } - string filePath = FileUtilities.NormalizePath(File.ItemSpec); - + ErrorUtilities.VerifyThrowArgumentLength(File.ItemSpec); + AbsolutePath filePath = FrameworkFileUtilities.NormalizePath(TaskEnvironment.GetAbsolutePath(File.ItemSpec)); string contentsAsString = string.Empty; if (Lines != null && Lines.Length > 0) @@ -122,7 +124,7 @@ public override bool Execute() } } - private bool ExecuteNonTransactional(string filePath, string directoryPath, string contentsAsString, Encoding encoding) + private bool ExecuteNonTransactional(AbsolutePath filePath, string directoryPath, string contentsAsString, Encoding encoding) { try { @@ -134,7 +136,7 @@ private bool ExecuteNonTransactional(string filePath, string directoryPath, stri { if (WriteOnlyWhenDifferent) { - Log.LogMessageFromResources(MessageImportance.Normal, "WriteLinesToFile.UnusedWriteOnlyWhenDifferent", filePath); + Log.LogMessageFromResources(MessageImportance.Normal, "WriteLinesToFile.UnusedWriteOnlyWhenDifferent", filePath.OriginalValue); } System.IO.File.AppendAllText(filePath, contentsAsString, encoding); @@ -145,12 +147,12 @@ private bool ExecuteNonTransactional(string filePath, string directoryPath, stri catch (Exception e) when (ExceptionHandling.IsIoRelatedException(e)) { string lockedFileMessage = LockCheck.GetLockedFileMessage(filePath); - Log.LogErrorWithCodeFromResources("WriteLinesToFile.ErrorOrWarning", filePath, e.Message, lockedFileMessage); + Log.LogErrorWithCodeFromResources("WriteLinesToFile.ErrorOrWarning", filePath.OriginalValue, e.Message, lockedFileMessage); return !Log.HasLoggedErrors; } } - private bool ExecuteTransactional(string filePath, string directoryPath, string contentsAsString, Encoding encoding) + private bool ExecuteTransactional(AbsolutePath filePath, string directoryPath, string contentsAsString, Encoding encoding) { try { @@ -162,7 +164,7 @@ private bool ExecuteTransactional(string filePath, string directoryPath, string { if (WriteOnlyWhenDifferent) { - Log.LogMessageFromResources(MessageImportance.Normal, "WriteLinesToFile.UnusedWriteOnlyWhenDifferent", filePath); + Log.LogMessageFromResources(MessageImportance.Normal, "WriteLinesToFile.UnusedWriteOnlyWhenDifferent", filePath.OriginalValue); } // For append mode, use atomic write to append only the new content @@ -173,7 +175,7 @@ private bool ExecuteTransactional(string filePath, string directoryPath, string catch (Exception e) when (ExceptionHandling.IsIoRelatedException(e)) { string lockedFileMessage = LockCheck.GetLockedFileMessage(filePath); - Log.LogErrorWithCodeFromResources("WriteLinesToFile.ErrorOrWarning", filePath, e.Message, lockedFileMessage); + Log.LogErrorWithCodeFromResources("WriteLinesToFile.ErrorOrWarning", filePath.OriginalValue, e.Message, lockedFileMessage); return !Log.HasLoggedErrors; } } @@ -182,7 +184,7 @@ private bool ExecuteTransactional(string filePath, string directoryPath, string /// Saves content to file atomically using a temporary file, following the Visual Studio editor pattern. /// This is for overwrite mode where we write the entire content. /// - private bool SaveAtomically(string filePath, string contentsAsString, Encoding encoding) + private bool SaveAtomically(AbsolutePath filePath, string contentsAsString, Encoding encoding) { string temporaryFilePath = null; try @@ -217,7 +219,7 @@ private bool SaveAtomically(string filePath, string contentsAsString, Encoding e { // Move failed, log and return string lockedFileMessage = LockCheck.GetLockedFileMessage(filePath); - Log.LogErrorWithCodeFromResources("WriteLinesToFile.ErrorOrWarning", filePath, moveEx.Message, lockedFileMessage); + Log.LogErrorWithCodeFromResources("WriteLinesToFile.ErrorOrWarning", filePath.OriginalValue, moveEx.Message, lockedFileMessage); return !Log.HasLoggedErrors; } } @@ -249,7 +251,7 @@ private bool SaveAtomically(string filePath, string contentsAsString, Encoding e catch (Exception fallbackEx) when (ExceptionHandling.IsIoRelatedException(fallbackEx)) { string lockedFileMessage = LockCheck.GetLockedFileMessage(filePath); - Log.LogErrorWithCodeFromResources("WriteLinesToFile.ErrorOrWarning", filePath, fallbackEx.Message, lockedFileMessage); + Log.LogErrorWithCodeFromResources("WriteLinesToFile.ErrorOrWarning", filePath.OriginalValue, fallbackEx.Message, lockedFileMessage); return !Log.HasLoggedErrors; } } @@ -257,7 +259,7 @@ private bool SaveAtomically(string filePath, string contentsAsString, Encoding e catch (Exception e) when (ExceptionHandling.IsIoRelatedException(e)) { string lockedFileMessage = LockCheck.GetLockedFileMessage(filePath); - Log.LogErrorWithCodeFromResources("WriteLinesToFile.ErrorOrWarning", filePath, e.Message, lockedFileMessage); + Log.LogErrorWithCodeFromResources("WriteLinesToFile.ErrorOrWarning", filePath.OriginalValue, e.Message, lockedFileMessage); return !Log.HasLoggedErrors; } finally @@ -284,7 +286,7 @@ private bool SaveAtomically(string filePath, string contentsAsString, Encoding e /// Appends content to file atomically. For append mode, we simply append the new content /// directly without reading the entire file, avoiding race conditions. /// - private bool SaveAtomicallyAppend(string filePath, string directoryPath, string contentsAsString, Encoding encoding) + private bool SaveAtomicallyAppend(AbsolutePath filePath, string directoryPath, string contentsAsString, Encoding encoding) { try { @@ -297,7 +299,7 @@ private bool SaveAtomicallyAppend(string filePath, string directoryPath, string catch (Exception e) when (ExceptionHandling.IsIoRelatedException(e)) { string lockedFileMessage = LockCheck.GetLockedFileMessage(filePath); - Log.LogErrorWithCodeFromResources("WriteLinesToFile.ErrorOrWarning", filePath, e.Message, lockedFileMessage); + Log.LogErrorWithCodeFromResources("WriteLinesToFile.ErrorOrWarning", filePath.OriginalValue, e.Message, lockedFileMessage); return !Log.HasLoggedErrors; } } @@ -306,7 +308,7 @@ private bool SaveAtomicallyAppend(string filePath, string directoryPath, string /// Checks if file should be written for Overwrite mode, considering WriteOnlyWhenDifferent option. /// /// True if file should be written, false if write should be skipped. - private bool ShouldWriteFileForOverwrite(string filePath, string contentsAsString) + private bool ShouldWriteFileForOverwrite(AbsolutePath filePath, string contentsAsString) { if (!WriteOnlyWhenDifferent) { @@ -321,24 +323,24 @@ private bool ShouldWriteFileForOverwrite(string filePath, string contentsAsStrin // Use stream-based comparison to avoid loading entire file into memory if (FilesAreIdentical(filePath, contentsAsString)) { - Log.LogMessageFromResources(MessageImportance.Low, "WriteLinesToFile.SkippingUnchangedFile", filePath); - MSBuildEventSource.Log.WriteLinesToFileUpToDateStop(filePath, true); + Log.LogMessageFromResources(MessageImportance.Low, "WriteLinesToFile.SkippingUnchangedFile", filePath.OriginalValue); + MSBuildEventSource.Log.WriteLinesToFileUpToDateStop(filePath.OriginalValue, true); return false; // Skip write - content is identical } else if (FailIfNotIncremental) { - Log.LogErrorWithCodeFromResources("WriteLinesToFile.ErrorReadingFile", filePath); - MSBuildEventSource.Log.WriteLinesToFileUpToDateStop(filePath, false); + Log.LogErrorWithCodeFromResources("WriteLinesToFile.ErrorReadingFile", filePath.OriginalValue); + MSBuildEventSource.Log.WriteLinesToFileUpToDateStop(filePath.OriginalValue, false); return false; // Skip write - file differs and FailIfNotIncremental is set } } } catch (IOException) { - Log.LogMessageFromResources(MessageImportance.Low, "WriteLinesToFile.ErrorReadingFile", filePath); + Log.LogMessageFromResources(MessageImportance.Low, "WriteLinesToFile.ErrorReadingFile", filePath.OriginalValue); } - MSBuildEventSource.Log.WriteLinesToFileUpToDateStop(filePath, false); + MSBuildEventSource.Log.WriteLinesToFileUpToDateStop(filePath.OriginalValue, false); return true; // Proceed with write } @@ -347,7 +349,7 @@ private bool ShouldWriteFileForOverwrite(string filePath, string contentsAsStrin /// Uses the default encoding for the comparison. /// /// True if file contents are identical to the provided string, false otherwise. - private bool FilesAreIdentical(string filePath, string contentsAsString) + private bool FilesAreIdentical(AbsolutePath filePath, string contentsAsString) { try { diff --git a/src/Tasks/FileState.cs b/src/Tasks/FileState.cs index 9021a946833..806dda2d893 100644 --- a/src/Tasks/FileState.cs +++ b/src/Tasks/FileState.cs @@ -4,6 +4,7 @@ using System; using System.IO; using System.Runtime.InteropServices; +using Microsoft.Build.Framework; using Microsoft.Build.Shared; #nullable disable @@ -226,16 +227,6 @@ public void ThrowException() } } - /// - /// The name of the file. - /// - private readonly string _filename; - - /// - /// Holds the full path equivalent of _filename - /// - public string FileNameFullPath; - /// /// Actual file or directory information /// @@ -245,11 +236,11 @@ public void ThrowException() /// Constructor. /// Only stores file name: does not grab the file state until first request. /// - internal FileState(string filename) + /// The normalized (absolute) path to the file. + internal FileState(AbsolutePath path) { - ErrorUtilities.VerifyThrowArgumentLength(filename); - _filename = filename; - _data = new Lazy(() => new FileDirInfo(_filename)); + Path = path; + _data = new Lazy(() => new FileDirInfo(Path)); } /// @@ -309,10 +300,18 @@ internal long Length } /// - /// Name of the file as it was passed in. - /// Not normalized. + /// Path of the file. /// - internal string Name => _filename; + internal AbsolutePath Path + { + get; + set + { + ErrorUtilities.VerifyThrowArgumentLength(value); + field = value; + _data = new Lazy(() => new FileDirInfo(value)); + } + } /// /// Whether this is a directory. @@ -333,7 +332,7 @@ internal bool IsDirectory /// internal void Reset() { - _data = new Lazy(() => new FileDirInfo(_filename)); + _data = new Lazy(() => new FileDirInfo(Path)); } } } diff --git a/src/Tasks/MakeDir.cs b/src/Tasks/MakeDir.cs index 8bc552bfd82..bf0c67ffa95 100644 --- a/src/Tasks/MakeDir.cs +++ b/src/Tasks/MakeDir.cs @@ -14,7 +14,8 @@ namespace Microsoft.Build.Tasks /// /// A task that creates a directory /// - public class MakeDir : TaskExtension, IIncrementalTask + [MSBuildMultiThreadableTask] + public class MakeDir : TaskExtension, IIncrementalTask, IMultiThreadableTask { [Required] public ITaskItem[] Directories @@ -33,6 +34,9 @@ public ITaskItem[] Directories public bool FailIfNotIncremental { get; set; } + /// + public TaskEnvironment TaskEnvironment { get; set; } + private ITaskItem[] _directories; #region ITask Members @@ -53,24 +57,26 @@ public override bool Execute() // here we check for that case. if (directory.ItemSpec.Length > 0) { + AbsolutePath? absolutePath = null; try { - // For speed, eliminate duplicates caused by poor targets authoring + // For speed, eliminate duplicates caused by poor targets authoring, don't absolutize yet to save allocation if (!directoriesSet.Contains(directory.ItemSpec)) { + absolutePath = TaskEnvironment.GetAbsolutePath(FrameworkFileUtilities.FixFilePath(directory.ItemSpec)); // Only log a message if we actually need to create the folder - if (!FileUtilities.DirectoryExistsNoThrow(directory.ItemSpec)) + if (!FileUtilities.DirectoryExistsNoThrow(absolutePath)) { if (FailIfNotIncremental) { - Log.LogErrorFromResources("MakeDir.Comment", directory.ItemSpec); + Log.LogErrorFromResources("MakeDir.Comment", absolutePath.Value.OriginalValue); } else { // Do not log a fake command line as well, as it's superfluous, and also potentially expensive - Log.LogMessageFromResources(MessageImportance.Normal, "MakeDir.Comment", directory.ItemSpec); + Log.LogMessageFromResources(MessageImportance.Normal, "MakeDir.Comment", absolutePath.Value.OriginalValue); - Directory.CreateDirectory(FrameworkFileUtilities.FixFilePath(directory.ItemSpec)); + Directory.CreateDirectory(absolutePath); } } @@ -79,7 +85,7 @@ public override bool Execute() } catch (Exception e) when (ExceptionHandling.IsIoRelatedException(e)) { - Log.LogErrorWithCodeFromResources("MakeDir.Error", directory.ItemSpec, e.Message); + Log.LogErrorWithCodeFromResources("MakeDir.Error", absolutePath?.OriginalValue ?? directory.ItemSpec, e.Message); } // Add even on failure to avoid reattempting diff --git a/src/Tasks/RemoveDir.cs b/src/Tasks/RemoveDir.cs index 3e43ca69a80..5eeda7f53d4 100644 --- a/src/Tasks/RemoveDir.cs +++ b/src/Tasks/RemoveDir.cs @@ -16,13 +16,17 @@ namespace Microsoft.Build.Tasks /// /// Remove the specified directories. /// - public class RemoveDir : TaskExtension, IIncrementalTask + [MSBuildMultiThreadableTask] + public class RemoveDir : TaskExtension, IIncrementalTask, IMultiThreadableTask { //----------------------------------------------------------------------------------- // Property: directory to remove //----------------------------------------------------------------------------------- private ITaskItem[] _directories; + /// + public TaskEnvironment TaskEnvironment { get; set; } + [Required] public ITaskItem[] Directories { @@ -61,31 +65,32 @@ public override bool Execute() continue; } - if (FileSystems.Default.DirectoryExists(directory.ItemSpec)) + AbsolutePath directoryPath = TaskEnvironment.GetAbsolutePath(directory.ItemSpec); + + if (FileSystems.Default.DirectoryExists(directoryPath)) { if (FailIfNotIncremental) { - Log.LogErrorFromResources("RemoveDir.Removing", directory.ItemSpec); + Log.LogErrorFromResources("RemoveDir.Removing", directoryPath.OriginalValue); continue; } // Do not log a fake command line as well, as it's superfluous, and also potentially expensive - Log.LogMessageFromResources(MessageImportance.Normal, "RemoveDir.Removing", directory.ItemSpec); - + Log.LogMessageFromResources(MessageImportance.Normal, "RemoveDir.Removing", directoryPath.OriginalValue); // Try to remove the directory, this will not log unauthorized access errors since // we will attempt to remove read only attributes and try again. - bool currentSuccess = RemoveDirectory(directory, false, out bool unauthorizedAccess); + bool currentSuccess = RemoveDirectory(directoryPath, false, out bool unauthorizedAccess); // The first attempt failed, to we will remove readonly attributes and try again.. if (!currentSuccess && unauthorizedAccess) { // If the directory delete operation returns an unauthorized access exception // we need to attempt to remove the readonly attributes and try again. - currentSuccess = RemoveReadOnlyAttributeRecursively(new DirectoryInfo(directory.ItemSpec)); + currentSuccess = RemoveReadOnlyAttributeRecursively(new DirectoryInfo(directoryPath)); if (currentSuccess) { // Retry the remove directory operation, this time we want to log any errors - currentSuccess = RemoveDirectory(directory, true, out unauthorizedAccess); + currentSuccess = RemoveDirectory(directoryPath, true, out unauthorizedAccess); } } @@ -99,7 +104,7 @@ public override bool Execute() } else { - Log.LogMessageFromResources(MessageImportance.Normal, "RemoveDir.SkippingNonexistentDirectory", directory.ItemSpec); + Log.LogMessageFromResources(MessageImportance.Normal, "RemoveDir.SkippingNonexistentDirectory", directoryPath.OriginalValue); // keep a running list of the directories that were actually removed // note that we include in this list directories that did not exist removedDirectoriesList.Add(new TaskItem(directory)); @@ -111,7 +116,7 @@ public override bool Execute() } // Core implementation of directory removal - private bool RemoveDirectory(ITaskItem directory, bool logUnauthorizedError, out bool unauthorizedAccess) + private bool RemoveDirectory(AbsolutePath directoryPath, bool logUnauthorizedError, out bool unauthorizedAccess) { bool success = true; @@ -120,7 +125,7 @@ private bool RemoveDirectory(ITaskItem directory, bool logUnauthorizedError, out try { // Try to delete the directory - Directory.Delete(directory.ItemSpec, true); + Directory.Delete(directoryPath, true); } catch (UnauthorizedAccessException e) { @@ -128,13 +133,13 @@ private bool RemoveDirectory(ITaskItem directory, bool logUnauthorizedError, out // Log the fact that there was a problem only if we have been asked to. if (logUnauthorizedError) { - Log.LogErrorWithCodeFromResources("RemoveDir.Error", directory, e.Message); + Log.LogErrorWithCodeFromResources("RemoveDir.Error", directoryPath.OriginalValue, e.Message); } unauthorizedAccess = true; } catch (Exception e) when (ExceptionHandling.IsIoRelatedException(e)) { - Log.LogErrorWithCodeFromResources("RemoveDir.Error", directory.ItemSpec, e.Message); + Log.LogErrorWithCodeFromResources("RemoveDir.Error", directoryPath.OriginalValue, e.Message); success = false; } diff --git a/src/Tasks/Touch.cs b/src/Tasks/Touch.cs index 8d157d48493..8f442777033 100644 --- a/src/Tasks/Touch.cs +++ b/src/Tasks/Touch.cs @@ -17,7 +17,8 @@ namespace Microsoft.Build.Tasks /// /// This class defines the touch task. /// - public class Touch : TaskExtension, IIncrementalTask + [MSBuildMultiThreadableTask] + public class Touch : TaskExtension, IIncrementalTask, IMultiThreadableTask { private MessageImportance messageImportance; @@ -48,6 +49,9 @@ public class Touch : TaskExtension, IIncrementalTask [Output] public ITaskItem[] TouchedFiles { get; set; } + /// + public TaskEnvironment TaskEnvironment { get; set; } + /// /// Importance: high, normal, low (default normal) /// @@ -91,7 +95,8 @@ internal bool ExecuteImpl( foreach (ITaskItem file in Files) { - string path = FrameworkFileUtilities.FixFilePath(file.ItemSpec); + AbsolutePath path = TaskEnvironment.GetAbsolutePath(FrameworkFileUtilities.FixFilePath(file.ItemSpec)); + // For speed, eliminate duplicates caused by poor targets authoring if (touchedFilesSet.Contains(path)) { @@ -164,7 +169,7 @@ public override bool Execute() /// /// "true" if the file was created. private bool CreateFile( - string file, + AbsolutePath file, FileCreate fileCreate) { try @@ -175,7 +180,7 @@ private bool CreateFile( } catch (Exception e) when (ExceptionHandling.IsIoRelatedException(e)) { - Log.LogErrorWithCodeFromResources("Touch.CannotCreateFile", file, e.Message); + Log.LogErrorWithCodeFromResources("Touch.CannotCreateFile", file.OriginalValue, e.Message); return false; } @@ -187,7 +192,7 @@ private bool CreateFile( /// /// "True" if the file was touched. private bool TouchFile( - string file, + AbsolutePath file, DateTime dt, FileExists fileExists, FileCreate fileCreate, @@ -203,11 +208,11 @@ private bool TouchFile( { if (FailIfNotIncremental) { - Log.LogWarningFromResources("Touch.CreatingFile", file, "AlwaysCreate"); + Log.LogWarningFromResources("Touch.CreatingFile", file.OriginalValue, "AlwaysCreate"); } else { - Log.LogMessageFromResources(messageImportance, "Touch.CreatingFile", file, "AlwaysCreate"); + Log.LogMessageFromResources(messageImportance, "Touch.CreatingFile", file.OriginalValue, "AlwaysCreate"); } if (!CreateFile(file, fileCreate)) @@ -217,18 +222,18 @@ private bool TouchFile( } else { - Log.LogErrorWithCodeFromResources("Touch.FileDoesNotExist", file); + Log.LogErrorWithCodeFromResources("Touch.FileDoesNotExist", file.OriginalValue); return false; } } if (FailIfNotIncremental) { - Log.LogWarningFromResources("Touch.Touching", file); + Log.LogWarningFromResources("Touch.Touching", file.OriginalValue); } else { - Log.LogMessageFromResources(messageImportance, "Touch.Touching", file); + Log.LogMessageFromResources(messageImportance, "Touch.Touching", file.OriginalValue); } // If the file is read only then we must either issue an error, or, if the user so @@ -248,7 +253,7 @@ private bool TouchFile( catch (Exception e) when (ExceptionHandling.IsIoRelatedException(e)) { string lockedFileMessage = LockCheck.GetLockedFileMessage(file); - Log.LogErrorWithCodeFromResources("Touch.CannotMakeFileWritable", file, e.Message, lockedFileMessage); + Log.LogErrorWithCodeFromResources("Touch.CannotMakeFileWritable", file.OriginalValue, e.Message, lockedFileMessage); return false; } } @@ -264,7 +269,7 @@ private bool TouchFile( catch (Exception e) when (ExceptionHandling.IsIoRelatedException(e)) { string lockedFileMessage = LockCheck.GetLockedFileMessage(file); - Log.LogErrorWithCodeFromResources("Touch.CannotTouch", file, e.Message, lockedFileMessage); + Log.LogErrorWithCodeFromResources("Touch.CannotTouch", file.OriginalValue, e.Message, lockedFileMessage); return false; } finally @@ -279,7 +284,7 @@ private bool TouchFile( } catch (Exception e) when (ExceptionHandling.IsIoRelatedException(e)) { - Log.LogErrorWithCodeFromResources("Touch.CannotRestoreAttributes", file, e.Message); + Log.LogErrorWithCodeFromResources("Touch.CannotRestoreAttributes", file.OriginalValue, e.Message); retVal = false; } }