From 56c86f1ab14fbe944c4d453380b63e8d6be4d57b Mon Sep 17 00:00:00 2001 From: Matthias Koch Date: Sun, 31 Mar 2024 04:10:42 +0200 Subject: [PATCH 1/5] feat(utilities): implement AbsolutePath copy/move --- source/Nuke.Common/IO/FileSystemTasks.cs | 9 + .../IO/FileSystemDependentTest.cs | 2 +- .../Nuke.Utilities.Tests/IO/MoveCopyTest.cs | 136 +++++++++++ .../Collections/Enumerable.WhereNotNull.cs | 9 + .../IO/AbsolutePath.MoveCopy.cs | 226 ++++++++++++++++-- 5 files changed, 360 insertions(+), 22 deletions(-) create mode 100644 source/Nuke.Utilities.Tests/IO/MoveCopyTest.cs diff --git a/source/Nuke.Common/IO/FileSystemTasks.cs b/source/Nuke.Common/IO/FileSystemTasks.cs index 48e9c24d4..a2062784f 100644 --- a/source/Nuke.Common/IO/FileSystemTasks.cs +++ b/source/Nuke.Common/IO/FileSystemTasks.cs @@ -188,6 +188,7 @@ public static void DeleteFile(string file) File.Delete(file); } + [Obsolete($"Use {nameof(AbsolutePath)}.{nameof(AbsolutePathExtensions.Copy)}")] public static void CopyFile(AbsolutePath source, AbsolutePath target, FileExistsPolicy policy = FileExistsPolicy.Fail, bool createDirectories = true) { if (!ShouldCopyFile(source, target, policy)) @@ -200,6 +201,7 @@ public static void CopyFile(AbsolutePath source, AbsolutePath target, FileExists File.Copy(source, target, overwrite: true); } + [Obsolete($"Use {nameof(AbsolutePath)}.{nameof(AbsolutePathExtensions.CopyToDirectory)}")] public static void CopyFileToDirectory( AbsolutePath source, AbsolutePath targetDirectory, @@ -209,6 +211,7 @@ public static void CopyFileToDirectory( CopyFile(source, Path.Combine(targetDirectory, Path.GetFileName(source).NotNull()), policy, createDirectories); } + [Obsolete($"Use {nameof(AbsolutePath)}.{nameof(AbsolutePathExtensions.Move)}")] public static void MoveFile(AbsolutePath source, AbsolutePath target, FileExistsPolicy policy = FileExistsPolicy.Fail, bool createDirectories = true) { if (!ShouldCopyFile(source, target, policy)) @@ -224,6 +227,7 @@ public static void MoveFile(AbsolutePath source, AbsolutePath target, FileExists File.Move(source, target); } + [Obsolete($"Use {nameof(AbsolutePath)}.{nameof(AbsolutePathExtensions.MoveToDirectory)}")] public static void MoveFileToDirectory( AbsolutePath source, AbsolutePath targetDirectory, @@ -233,6 +237,7 @@ public static void MoveFileToDirectory( MoveFile(source, Path.Combine(targetDirectory, Path.GetFileName(source).NotNull()), policy, createDirectories); } + [Obsolete($"Use {nameof(AbsolutePath)}.{nameof(AbsolutePathExtensions.Rename)}")] public static void RenameFile(AbsolutePath file, string newName, FileExistsPolicy policy = FileExistsPolicy.Fail) { if (Path.GetFileName(file) == newName) @@ -241,6 +246,7 @@ public static void RenameFile(AbsolutePath file, string newName, FileExistsPolic MoveFile(file, Path.Combine(Path.GetDirectoryName(file).NotNull(), newName), policy); } + [Obsolete($"Use {nameof(AbsolutePath)}.{nameof(AbsolutePathExtensions.Move)}")] public static void MoveDirectory( AbsolutePath source, AbsolutePath target, @@ -264,6 +270,7 @@ public static void MoveDirectory( } } + [Obsolete($"Use {nameof(AbsolutePath)}.{nameof(AbsolutePathExtensions.MoveToDirectory)}")] public static void MoveDirectoryToDirectory( AbsolutePath source, AbsolutePath targetDirectory, @@ -273,6 +280,7 @@ public static void MoveDirectoryToDirectory( MoveDirectory(source, Path.Combine(targetDirectory, new DirectoryInfo(source).Name), directoryPolicy, filePolicy); } + [Obsolete($"Use {nameof(AbsolutePath)}.{nameof(AbsolutePathExtensions.Rename)}")] public static void RenameDirectory( string directory, string newName, @@ -282,6 +290,7 @@ public static void RenameDirectory( MoveDirectory(directory, Path.Combine(Path.GetDirectoryName(directory).NotNull(), newName), directoryPolicy, filePolicy); } + [Obsolete($"Use {nameof(AbsolutePath)}.{nameof(AbsolutePathExtensions.Copy)}")] public static void CopyDirectoryRecursively( AbsolutePath source, AbsolutePath target, diff --git a/source/Nuke.Utilities.Tests/IO/FileSystemDependentTest.cs b/source/Nuke.Utilities.Tests/IO/FileSystemDependentTest.cs index 927d2dce1..8e721a507 100644 --- a/source/Nuke.Utilities.Tests/IO/FileSystemDependentTest.cs +++ b/source/Nuke.Utilities.Tests/IO/FileSystemDependentTest.cs @@ -32,7 +32,7 @@ protected FileSystemDependentTest(ITestOutputHelper testOutputHelper) ExecutionDirectory = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location).NotNull(); RootDirectory = Constants.TryGetRootDirectoryFrom(EnvironmentInfo.WorkingDirectory); TestProjectDirectory = ExecutionDirectory.FindParentOrSelf(x => x.ContainsFile("*.csproj")); - TestTempDirectory = ExecutionDirectory / "temp" / $"{GetType().Name}.{TestName}"; + TestTempDirectory = ExecutionDirectory / "temp" / $"{GetType().Name}.{TestName}"; TestTempDirectory.CreateOrCleanDirectory(); } diff --git a/source/Nuke.Utilities.Tests/IO/MoveCopyTest.cs b/source/Nuke.Utilities.Tests/IO/MoveCopyTest.cs new file mode 100644 index 000000000..c9e332022 --- /dev/null +++ b/source/Nuke.Utilities.Tests/IO/MoveCopyTest.cs @@ -0,0 +1,136 @@ +// Copyright 2024 Maintainers of NUKE. +// Distributed under the MIT License. +// https://github.com/nuke-build/nuke/blob/master/LICENSE + +using System; +using System.Linq; +using FluentAssertions; +using Nuke.Common.IO; +using Nuke.Common.Utilities.Collections; +using Xunit; +using Xunit.Abstractions; + +namespace Nuke.Common.Tests; + +public class MoveCopyTest : FileSystemDependentTest +{ + public MoveCopyTest(ITestOutputHelper testOutputHelper) : base(testOutputHelper) + { + AbsolutePathExtensions.DefaultEofLineBreak = false; + } + + [Fact] + public void TestCopyFile() + { + var source = TestTempDirectory / "source.txt"; + source.WriteAllText("foobar"); + + var target = TestTempDirectory / "target.txt"; + source.Copy(target); + + target.FileExists().Should().BeTrue(); + + new Action(() => source.Copy(target)) + .Should().Throw().WithMessage("* already exists"); + + new Action(() => source.Copy(target, policy: ExistsPolicy.FileFail | ExistsPolicy.FileOverwrite)) + .Should().Throw().WithMessage("Multiple file policies *"); + + source.WriteAllText("fizzbuzz"); + source.Copy(target, policy: ExistsPolicy.FileOverwrite) + .Should().Be(target); + target.ReadAllText().Should().Be("fizzbuzz"); + } + + [Fact] + public void TestMoveFile() + { + var source1 = TestTempDirectory / "source1.txt"; + var source2 = TestTempDirectory / "source2.txt"; + var source3 = TestTempDirectory / "source3.txt"; + source1.WriteAllText(nameof(source1)); + source2.WriteAllText(nameof(source2)); + source3.WriteAllText(nameof(source3)); + + var target = TestTempDirectory / "target.txt"; + source2.Move(target); + + target.FileExists().Should().BeTrue(); + source2.FileExists().Should().BeFalse(); + + new Action(() => source1.Move(target, policy: ExistsPolicy.FileFail)) + .Should().Throw().WithMessage("* already exists"); + + source1.Move(target, policy: ExistsPolicy.FileSkip).Should().Be(source1); + source1.Move(target, policy: ExistsPolicy.FileOverwriteIfNewer).Should().Be(source1); + source3.Move(target, policy: ExistsPolicy.FileOverwriteIfNewer).Should().Be(target); + } + + [Fact] + public void TestCopyDirectory() + { + var source = TestTempDirectory / "source"; + var sourceFiles = new[] + { + source / "source1.txt", + source / "source2.txt", + source / "sub" / "source3.txt", + source / "sub" / "source4.txt", + }; + sourceFiles.ForEach(x => x.WriteAllText("source")); + + var target = TestTempDirectory / "target"; + source.Copy(target); + target.GetFiles(depth: int.MaxValue).Select(x => target.GetRelativePathTo(x).ToString()) + .Should().BeEquivalentTo(sourceFiles.Select(x => source.GetRelativePathTo(x).ToString())); + + target.CreateOrCleanDirectory(); + var target0 = (target / "source0.txt").TouchFile(); + var target3 = (target / "sub" / "source3.txt").WriteAllText("target"); + var target4 = (target / "sub" / "source4.txt").WriteAllText("target"); + (source / target.GetRelativePathTo(target4)).TouchFile(); + + new Action(() => source.Copy(target, ExistsPolicy.DirectoryFail)) + .Should().Throw().WithMessage("Policy disallows merging directories"); + target.GetFiles(depth: int.MaxValue).Should().HaveCount(3); + + source.Copy(target, ExistsPolicy.MergeAndSkip); + target0.FileExists().Should().BeTrue(); + target3.ReadAllText().Should().Be("target"); + target4.ReadAllText().Should().Be("target"); + + source.Copy(target, ExistsPolicy.MergeAndOverwriteIfNewer); + target3.ReadAllText().Should().Be("target"); + target4.ReadAllText().Should().Be("source"); + + source.Copy(target, ExistsPolicy.MergeAndOverwrite); + target3.ReadAllText().Should().Be("source"); + } + + [Fact] + public void TestMoveDirectory() + { + var source = TestTempDirectory / "source"; + var sourceFiles = new[] + { + source / "source1.txt", + source / "source2.txt", + source / "sub" / "source3.txt", + source / "sub" / "source4.txt", + }; + sourceFiles.ForEach(x => x.WriteAllText("source")); + + var target = TestTempDirectory / "target"; + (target / "source1.txt").TouchFile(); + (target / "sub" / "source3.txt").TouchFile(); + + new Action(() => source.Move(target)).Should().Throw(); + + source.Move(target, ExistsPolicy.MergeAndSkip); + source.GetFiles(depth: int.MaxValue).Should().HaveCount(2); + + source.Move(target, ExistsPolicy.MergeAndSkip, deleteRemainingFiles: true) + .Should().Be(target); + source.DirectoryExists().Should().BeFalse(); + } +} diff --git a/source/Nuke.Utilities/Collections/Enumerable.WhereNotNull.cs b/source/Nuke.Utilities/Collections/Enumerable.WhereNotNull.cs index 7e480da59..4ee4180ae 100644 --- a/source/Nuke.Utilities/Collections/Enumerable.WhereNotNull.cs +++ b/source/Nuke.Utilities/Collections/Enumerable.WhereNotNull.cs @@ -18,4 +18,13 @@ public static IEnumerable WhereNotNull(this IEnumerable enumerable) { return enumerable.Where(x => x != null); } + + /// + /// Filters the collection to elements that don't meet the condition. + /// + public static IEnumerable WhereNot(this IEnumerable enumerable, Func condition) + where T : class + { + return enumerable.Where(x => condition == null || !condition(x)); + } } diff --git a/source/Nuke.Utilities/IO/AbsolutePath.MoveCopy.cs b/source/Nuke.Utilities/IO/AbsolutePath.MoveCopy.cs index 7535a7a54..ec1c3e931 100644 --- a/source/Nuke.Utilities/IO/AbsolutePath.MoveCopy.cs +++ b/source/Nuke.Utilities/IO/AbsolutePath.MoveCopy.cs @@ -4,73 +4,257 @@ using System; using System.IO; +using System.Linq; +using Nuke.Common.Utilities.Collections; namespace Nuke.Common.IO; +[Flags] +public enum ExistsPolicy +{ + DirectoryFail = 1, + DirectoryMerge = 2, + FileFail = 4, + FileSkip = 8, + FileOverwrite = 16, + FileOverwriteIfNewer = 32, + + Fail = DirectoryFail | FileFail, + MergeAndSkip = DirectoryMerge | FileSkip, + MergeAndOverwrite = DirectoryMerge | FileOverwrite, + MergeAndOverwriteIfNewer = DirectoryMerge | FileOverwriteIfNewer +} + partial class AbsolutePathExtensions { /// /// Renames the file or directory. /// - public static AbsolutePath Rename(this AbsolutePath path, string newName) + public static AbsolutePath Rename( + this AbsolutePath source, + string newName, + ExistsPolicy policy = ExistsPolicy.Fail) { - return path.Move(path.Parent / newName); + return source.Move(source.Parent / newName, policy); } /// /// Renames the file or directory. /// - public static AbsolutePath Rename(this AbsolutePath path, Func newName) + public static AbsolutePath Rename( + this AbsolutePath source, + Func newName, + ExistsPolicy policy = ExistsPolicy.Fail) { - return path.Rename(newName.Invoke(path)); + return source.Rename(newName.Invoke(source), policy); } /// /// Renames the file without changing the extension. /// - public static AbsolutePath RenameWithoutExtension(this AbsolutePath path, string newName) + public static AbsolutePath RenameWithoutExtension( + this AbsolutePath source, + string newName, + ExistsPolicy policy = ExistsPolicy.Fail) { - Assert.True(path.FileExists()); - return path.Move(path.Parent / newName + path.Extension); + return source.Move(source.Parent / newName + source.Extension, policy); } /// /// Renames the file without changing the extension. /// - public static AbsolutePath RenameWithoutExtension(this AbsolutePath path, Func newName) + public static AbsolutePath RenameWithoutExtension( + this AbsolutePath source, + Func newName, + ExistsPolicy policy = ExistsPolicy.Fail) { - return path.RenameWithoutExtension(newName.Invoke(path)); + return source.RenameWithoutExtension(newName.Invoke(source), policy); } /// /// Moves the file or directory to another directory. /// - public static AbsolutePath MoveToDirectory(this AbsolutePath path, AbsolutePath directory) + public static AbsolutePath MoveToDirectory( + this AbsolutePath source, + AbsolutePath target, + ExistsPolicy policy = ExistsPolicy.Fail, + bool createDirectories = true) { - Assert.True(directory.Exists()); - return path.Move(directory / path.Name); + return source.Move(target / source.Name, policy, createDirectories); } /// /// Moves the file or directory. /// - public static AbsolutePath Move(this AbsolutePath path, Func newPath) + public static AbsolutePath Move( + this AbsolutePath source, + Func newPath, + ExistsPolicy policy = ExistsPolicy.Fail, + bool createDirectories = true) { - return path.Move(newPath.Invoke(path)); + return source.Move(newPath.Invoke(source), policy, createDirectories); } /// /// Moves the file or directory. /// - public static AbsolutePath Move(this AbsolutePath path, AbsolutePath newPath) + public static AbsolutePath Move( + this AbsolutePath source, + AbsolutePath target, + ExistsPolicy policy = ExistsPolicy.Fail, + bool createDirectories = true, + bool deleteRemainingFiles = false) + { + Assert.True(source.DirectoryExists() || source.FileExists()); + + if (source.DirectoryExists()) + return MoveDirectory(source, target, policy, createDirectories, deleteRemainingFiles); + + if (source.FileExists()) + return MoveFile(source, target, policy, createDirectories); + + throw new Exception("Unreachable"); + } + + /// + /// Copies the file or directory to another directory. + /// + public static AbsolutePath CopyToDirectory( + this AbsolutePath source, + AbsolutePath target, + ExistsPolicy policy = ExistsPolicy.Fail, + Func excludeDirectory = null, + Func excludeFile = null, + bool createDirectories = true) + { + return source.Copy(target / source.Name, policy, excludeDirectory, excludeFile, createDirectories); + } + + /// + /// Copies the file or directory. + /// + public static AbsolutePath Copy( + this AbsolutePath source, + AbsolutePath target, + ExistsPolicy policy = ExistsPolicy.Fail, + Func excludeDirectory = null, + Func excludeFile = null, + bool createDirectories = true) + { + Assert.True(source.DirectoryExists() || source.FileExists()); + + if (source.DirectoryExists()) + return CopyDirectory(source, target, policy, excludeDirectory, excludeFile, createDirectories); + + if (source.FileExists()) + return CopyFile(source, target, policy, createDirectories); + + throw new Exception("Unreachable"); + } + + private static AbsolutePath MoveFile( + this AbsolutePath source, + AbsolutePath target, + ExistsPolicy policy = ExistsPolicy.Fail, + bool createDirectories = true) + { + return HandleFile(source, target, policy, createDirectories, () => + { + target.DeleteFile(); + File.Move(source, target); + }); + } + + private static AbsolutePath CopyFile( + this AbsolutePath source, + AbsolutePath target, + ExistsPolicy policy = ExistsPolicy.Fail, + bool createDirectories = true) + { + return HandleFile(source, target, policy, createDirectories, () => + { + File.Copy(source, target, overwrite: true); + }); + } + + private static AbsolutePath HandleFile( + AbsolutePath source, + AbsolutePath target, + ExistsPolicy policy, + bool createDirectories, + Action action) + { + if (File.Exists(target) && !Permitted()) + return source; + + if (createDirectories) + target.Parent.CreateDirectory(); + + action.Invoke(); + return target; + + bool Permitted() + { + var filePolicies = ExistsPolicy.FileFail | ExistsPolicy.FileSkip | ExistsPolicy.FileOverwrite | ExistsPolicy.FileOverwriteIfNewer; + return (policy & filePolicies) switch + { + ExistsPolicy.FileFail => throw new Exception($"File '{target}' already exists"), + ExistsPolicy.FileSkip => false, + ExistsPolicy.FileOverwrite => true, + ExistsPolicy.FileOverwriteIfNewer => File.GetLastWriteTimeUtc(target) < File.GetLastWriteTimeUtc(source), + _ => throw new ArgumentOutOfRangeException(nameof(policy), policy, message: "Multiple file policies set") + }; + } + } + + private static AbsolutePath MoveDirectory( + this AbsolutePath source, + AbsolutePath target, + ExistsPolicy policy = ExistsPolicy.Fail, + bool createDirectories = true, + bool deleteRemainingFiles = false) + { + return HandleDirectory(source, target, policy, createDirectories, () => + { + source.GetDirectories().ForEach(x => x.MoveDirectory(target / source.GetRelativePathTo(x), policy)); + source.GetFiles().ForEach(x => x.MoveFile(target / source.GetRelativePathTo(x), policy)); + + if (!source.ToDirectoryInfo().EnumerateFileSystemInfos().Any() || deleteRemainingFiles) + source.DeleteDirectory(); + }); + } + + private static AbsolutePath CopyDirectory( + this AbsolutePath source, + AbsolutePath target, + ExistsPolicy policy = ExistsPolicy.Fail, + Func excludeDirectory = null, + Func excludeFile = null, + bool createDirectories = true) + { + return HandleDirectory(source, target, policy, createDirectories, () => + { + source.GetDirectories().WhereNot(excludeDirectory).ForEach(x => x.CopyDirectory(target / source.GetRelativePathTo(x), policy, excludeDirectory, excludeFile)); + source.GetFiles().WhereNot(excludeFile).ForEach(x => x.CopyFile(target / source.GetRelativePathTo(x), policy)); + }); + } + + private static AbsolutePath HandleDirectory( + AbsolutePath source, + AbsolutePath target, + ExistsPolicy policy, + bool createDirectories, + Action action) { - Assert.True(path.DirectoryExists() || path.FileExists()); + Assert.DirectoryExists(source); + Assert.False(source.Contains(target), $"Target directory '{target}' must not be in source directory '{source}'"); + Assert.True(!Directory.Exists(target) || (policy.HasFlag(ExistsPolicy.DirectoryMerge) && !policy.HasFlag(ExistsPolicy.DirectoryFail)), + "Policy disallows merging directories"); - if (path.DirectoryExists()) - Directory.Move(path, newPath); - else if (path.FileExists()) - File.Move(path, newPath); + if (createDirectories) + target.CreateDirectory(); - return path; + action.Invoke(); + return target; } } From f97cb2440a18fe41941cb946bc66adc20f29631f Mon Sep 17 00:00:00 2001 From: Matthias Koch Date: Sun, 31 Mar 2024 04:18:21 +0200 Subject: [PATCH 2/5] chore: update FileSystemTasks usages --- build/Build.GlobalSolution.cs | 11 ++++------- source/Nuke.Common/Tools/ReSharper/ReSharperTasks.cs | 8 +++----- source/Nuke.Common/Utilities/TemplateUtility.cs | 10 ++-------- 3 files changed, 9 insertions(+), 20 deletions(-) diff --git a/build/Build.GlobalSolution.cs b/build/Build.GlobalSolution.cs index c601c8838..4e2107570 100644 --- a/build/Build.GlobalSolution.cs +++ b/build/Build.GlobalSolution.cs @@ -15,7 +15,6 @@ using Nuke.Common.Utilities; using Nuke.Utilities.Text.Yaml; using static Nuke.Common.ControlFlow; -using static Nuke.Common.IO.FileSystemTasks; using static Nuke.Common.ProjectModel.SolutionModelTasks; using static Nuke.Common.Tools.Git.GitTasks; @@ -67,18 +66,16 @@ IEnumerable ExternalRepositories if ((RootDirectory / $"{Solution.FileName}.DotSettings").FileExists()) { - CopyFile( - source: RootDirectory / $"{Solution.FileName}.DotSettings", + (RootDirectory / $"{Solution.FileName}.DotSettings").Copy( target: RootDirectory / $"{global.FileName}.DotSettings", - FileExistsPolicy.Overwrite); + policy: ExistsPolicy.FileOverwrite); } if ((RootDirectory / $"{Solution.FileName}.DotSettings.user").FileExists()) { - CopyFile( - source: RootDirectory / $"{Solution.FileName}.DotSettings.user", + (RootDirectory / $"{Solution.FileName}.DotSettings.user").Copy( target: RootDirectory / $"{global.FileName}.DotSettings.user", - FileExistsPolicy.Overwrite); + policy: ExistsPolicy.FileOverwrite); } }); } diff --git a/source/Nuke.Common/Tools/ReSharper/ReSharperTasks.cs b/source/Nuke.Common/Tools/ReSharper/ReSharperTasks.cs index 67b246bf3..23f8375b8 100644 --- a/source/Nuke.Common/Tools/ReSharper/ReSharperTasks.cs +++ b/source/Nuke.Common/Tools/ReSharper/ReSharperTasks.cs @@ -26,11 +26,9 @@ private static void PreProcess(ref T toolSettings) where T : ReSharperSetting var wave = GetWave(toolSettings).NotNull("wave != null"); var shadowDirectory = GetShadowDirectory(toolSettings, wave); - FileSystemTasks.CopyDirectoryRecursively( - Path.GetDirectoryName(toolSettings.ProcessToolPath).NotNull(), - shadowDirectory, - DirectoryExistsPolicy.Merge, - FileExistsPolicy.OverwriteIfNewer); + ((AbsolutePath)toolSettings.ProcessToolPath.NotNull()).Copy( + target: shadowDirectory, + policy: ExistsPolicy.MergeAndOverwriteIfNewer); toolSettings.Plugins .Select(x => (Plugin: x.Key, Version: x.Value == ReSharperPluginLatest ? null : x.Value)) diff --git a/source/Nuke.Common/Utilities/TemplateUtility.cs b/source/Nuke.Common/Utilities/TemplateUtility.cs index f8fc14758..7dc604d5c 100644 --- a/source/Nuke.Common/Utilities/TemplateUtility.cs +++ b/source/Nuke.Common/Utilities/TemplateUtility.cs @@ -88,20 +88,14 @@ private static void FillTemplateDirectoryRecursivelyInternal( FillTemplateFile(file, tokens); if (ShouldMove(file)) - FileSystemTasks.RenameFile(file, file.Name.Replace(tokens), FileExistsPolicy.OverwriteIfNewer); + file.Rename(file.Name.Replace(tokens), ExistsPolicy.FileOverwriteIfNewer); } directory.GetDirectories() .ForEach(x => FillTemplateDirectoryRecursivelyInternal(x, tokens, excludeDirectory, excludeFile)); if (ShouldMove(directory)) - { - FileSystemTasks.RenameDirectory( - directory, - directory.Name.Replace(tokens), - DirectoryExistsPolicy.Merge, - FileExistsPolicy.OverwriteIfNewer); - } + directory.Rename(directory.Name.Replace(tokens), ExistsPolicy.MergeAndOverwriteIfNewer); } public static void FillTemplateFile( From ea39775885afa1db8975feee64f770bdb2d3d3fb Mon Sep 17 00:00:00 2001 From: Matthias Koch Date: Sun, 31 Mar 2024 04:42:54 +0200 Subject: [PATCH 3/5] fix --- source/Nuke.Utilities.Tests/IO/MoveCopyTest.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/source/Nuke.Utilities.Tests/IO/MoveCopyTest.cs b/source/Nuke.Utilities.Tests/IO/MoveCopyTest.cs index c9e332022..f0a559698 100644 --- a/source/Nuke.Utilities.Tests/IO/MoveCopyTest.cs +++ b/source/Nuke.Utilities.Tests/IO/MoveCopyTest.cs @@ -63,6 +63,8 @@ public void TestMoveFile() source1.Move(target, policy: ExistsPolicy.FileSkip).Should().Be(source1); source1.Move(target, policy: ExistsPolicy.FileOverwriteIfNewer).Should().Be(source1); + + source3.TouchFile(); source3.Move(target, policy: ExistsPolicy.FileOverwriteIfNewer).Should().Be(target); } From 6eb779618e22b0cdf1fa27538b305f750b698d88 Mon Sep 17 00:00:00 2001 From: Matthias Koch Date: Sun, 31 Mar 2024 04:45:21 +0200 Subject: [PATCH 4/5] fix --- source/Nuke.Utilities.Tests/IO/MoveCopyTest.cs | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/source/Nuke.Utilities.Tests/IO/MoveCopyTest.cs b/source/Nuke.Utilities.Tests/IO/MoveCopyTest.cs index f0a559698..85f1c74b2 100644 --- a/source/Nuke.Utilities.Tests/IO/MoveCopyTest.cs +++ b/source/Nuke.Utilities.Tests/IO/MoveCopyTest.cs @@ -45,12 +45,9 @@ public void TestCopyFile() [Fact] public void TestMoveFile() { - var source1 = TestTempDirectory / "source1.txt"; - var source2 = TestTempDirectory / "source2.txt"; - var source3 = TestTempDirectory / "source3.txt"; - source1.WriteAllText(nameof(source1)); - source2.WriteAllText(nameof(source2)); - source3.WriteAllText(nameof(source3)); + var source1 = (TestTempDirectory / "source1.txt").TouchFile(); + var source2 = (TestTempDirectory / "source2.txt").TouchFile(); + var source3 = (TestTempDirectory / "source3.txt").TouchFile(); var target = TestTempDirectory / "target.txt"; source2.Move(target); From 694f7fcc92102cf889e668ec00adbb1740817415 Mon Sep 17 00:00:00 2001 From: Leor Greenberger <16126495+leorg99@users.noreply.github.com> Date: Thu, 11 Apr 2024 17:14:46 -0400 Subject: [PATCH 5/5] Add ability to specify labels and group for self-hosted runners --- ...ribute=GitHubActionsAttribute.verified.txt | 89 ++++++++++++++++++ ...ribute=GitHubActionsAttribute.verified.txt | 91 +++++++++++++++++++ .../CI/ConfigurationGenerationTest.cs | 35 +++++++ .../Nuke.Common.Tests.csproj | 7 ++ .../Configuration/GitHubActionsJob.cs | 39 +++++++- .../GithubActionsCustomWriterExtensions.cs | 28 ++++++ .../GitHubActions/GitHubActionsAttribute.cs | 5 + 7 files changed, 292 insertions(+), 2 deletions(-) create mode 100644 source/Nuke.Common.Tests/CI/ConfigurationGenerationTest.Test_testName=self-hosted-NoCustomGroup_attribute=GitHubActionsAttribute.verified.txt create mode 100644 source/Nuke.Common.Tests/CI/ConfigurationGenerationTest.Test_testName=self-hosted_attribute=GitHubActionsAttribute.verified.txt create mode 100644 source/Nuke.Common/CI/GitHubActions/Configuration/GithubActionsCustomWriterExtensions.cs diff --git a/source/Nuke.Common.Tests/CI/ConfigurationGenerationTest.Test_testName=self-hosted-NoCustomGroup_attribute=GitHubActionsAttribute.verified.txt b/source/Nuke.Common.Tests/CI/ConfigurationGenerationTest.Test_testName=self-hosted-NoCustomGroup_attribute=GitHubActionsAttribute.verified.txt new file mode 100644 index 000000000..bfa375686 --- /dev/null +++ b/source/Nuke.Common.Tests/CI/ConfigurationGenerationTest.Test_testName=self-hosted-NoCustomGroup_attribute=GitHubActionsAttribute.verified.txt @@ -0,0 +1,89 @@ +# ------------------------------------------------------------------------------ +# +# +# This code was generated. +# +# - To turn off auto-generation set: +# +# [TestGitHubActions (AutoGenerate = false)] +# +# - To trigger manual generation invoke: +# +# nuke --generate-configuration GitHubActions_test --host GitHubActions +# +# +# ------------------------------------------------------------------------------ + +name: test + +on: [push, pull_request] + +permissions: + contents: write + actions: read + +jobs: + ubuntu-latest: + name: ubuntu-latest + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: 'Cache: .nuke/temp, ~/.nuget/packages' + uses: actions/cache@v4 + with: + path: | + .nuke/temp + ~/.nuget/packages + key: ${{ runner.os }}-${{ hashFiles('**/global.json', '**/*.csproj', '**/Directory.Packages.props') }} + - name: 'Run: Test' + run: ./build.cmd Test + env: + ApiKey: ${{ secrets.API_KEY }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: 'Publish: src' + uses: actions/upload-artifact@v4 + with: + name: src + path: src + - name: 'Publish: test-results' + uses: actions/upload-artifact@v4 + with: + name: test-results + path: output/test-results + - name: 'Publish: coverage-report.zip' + uses: actions/upload-artifact@v4 + with: + name: coverage-report.zip + path: output/coverage-report.zip + self-hosted: + name: self-hosted + runs-on: [ self-hosted, label1, label2 ] + steps: + - uses: actions/checkout@v3 + - name: 'Cache: .nuke/temp, ~/.nuget/packages' + uses: actions/cache@v4 + with: + path: | + .nuke/temp + ~/.nuget/packages + key: ${{ runner.os }}-${{ hashFiles('**/global.json', '**/*.csproj', '**/Directory.Packages.props') }} + - name: 'Run: Test' + run: ./build.cmd Test + env: + ApiKey: ${{ secrets.API_KEY }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: 'Publish: src' + uses: actions/upload-artifact@v4 + with: + name: src + path: src + - name: 'Publish: test-results' + uses: actions/upload-artifact@v4 + with: + name: test-results + path: output/test-results + - name: 'Publish: coverage-report.zip' + uses: actions/upload-artifact@v4 + with: + name: coverage-report.zip + path: output/coverage-report.zip diff --git a/source/Nuke.Common.Tests/CI/ConfigurationGenerationTest.Test_testName=self-hosted_attribute=GitHubActionsAttribute.verified.txt b/source/Nuke.Common.Tests/CI/ConfigurationGenerationTest.Test_testName=self-hosted_attribute=GitHubActionsAttribute.verified.txt new file mode 100644 index 000000000..ea1b7bd1d --- /dev/null +++ b/source/Nuke.Common.Tests/CI/ConfigurationGenerationTest.Test_testName=self-hosted_attribute=GitHubActionsAttribute.verified.txt @@ -0,0 +1,91 @@ +# ------------------------------------------------------------------------------ +# +# +# This code was generated. +# +# - To turn off auto-generation set: +# +# [TestGitHubActions (AutoGenerate = false)] +# +# - To trigger manual generation invoke: +# +# nuke --generate-configuration GitHubActions_test --host GitHubActions +# +# +# ------------------------------------------------------------------------------ + +name: test + +on: [push, pull_request] + +permissions: + contents: write + actions: read + +jobs: + ubuntu-latest: + name: ubuntu-latest + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: 'Cache: .nuke/temp, ~/.nuget/packages' + uses: actions/cache@v4 + with: + path: | + .nuke/temp + ~/.nuget/packages + key: ${{ runner.os }}-${{ hashFiles('**/global.json', '**/*.csproj', '**/Directory.Packages.props') }} + - name: 'Run: Test' + run: ./build.cmd Test + env: + ApiKey: ${{ secrets.API_KEY }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: 'Publish: src' + uses: actions/upload-artifact@v4 + with: + name: src + path: src + - name: 'Publish: test-results' + uses: actions/upload-artifact@v4 + with: + name: test-results + path: output/test-results + - name: 'Publish: coverage-report.zip' + uses: actions/upload-artifact@v4 + with: + name: coverage-report.zip + path: output/coverage-report.zip + self-hosted: + name: self-hosted + runs-on: + group: SomeGroup + labels: [ self-hosted, label1, label2 ] + steps: + - uses: actions/checkout@v3 + - name: 'Cache: .nuke/temp, ~/.nuget/packages' + uses: actions/cache@v4 + with: + path: | + .nuke/temp + ~/.nuget/packages + key: ${{ runner.os }}-${{ hashFiles('**/global.json', '**/*.csproj', '**/Directory.Packages.props') }} + - name: 'Run: Test' + run: ./build.cmd Test + env: + ApiKey: ${{ secrets.API_KEY }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: 'Publish: src' + uses: actions/upload-artifact@v4 + with: + name: src + path: src + - name: 'Publish: test-results' + uses: actions/upload-artifact@v4 + with: + name: test-results + path: output/test-results + - name: 'Publish: coverage-report.zip' + uses: actions/upload-artifact@v4 + with: + name: coverage-report.zip + path: output/coverage-report.zip diff --git a/source/Nuke.Common.Tests/CI/ConfigurationGenerationTest.cs b/source/Nuke.Common.Tests/CI/ConfigurationGenerationTest.cs index 8d3ffbbca..b847fc1f3 100644 --- a/source/Nuke.Common.Tests/CI/ConfigurationGenerationTest.cs +++ b/source/Nuke.Common.Tests/CI/ConfigurationGenerationTest.cs @@ -162,6 +162,41 @@ public class TestBuild : NukeBuild } ); + yield return + ( + "self-hosted", + new TestGitHubActionsAttribute( + GitHubActionsImage.UbuntuLatest, + GitHubActionsImage.SelfHosted) + { + On = new[] { GitHubActionsTrigger.Push, GitHubActionsTrigger.PullRequest }, + CustomRunnerLabels = ["label1", "label2"], + CustomRunnerGroup = "SomeGroup", + InvokedTargets = new[] { nameof(Test) }, + ImportSecrets = new[] { nameof(ApiKey) }, + EnableGitHubToken = true, + WritePermissions = new[] { GitHubActionsPermissions.Contents }, + ReadPermissions = new[] { GitHubActionsPermissions.Actions } + } + ); + + yield return + ( + "self-hosted-NoCustomGroup", + new TestGitHubActionsAttribute( + GitHubActionsImage.UbuntuLatest, + GitHubActionsImage.SelfHosted) + { + On = new[] { GitHubActionsTrigger.Push, GitHubActionsTrigger.PullRequest }, + CustomRunnerLabels = ["label1", "label2"], + InvokedTargets = new[] { nameof(Test) }, + ImportSecrets = new[] { nameof(ApiKey) }, + EnableGitHubToken = true, + WritePermissions = new[] { GitHubActionsPermissions.Contents }, + ReadPermissions = new[] { GitHubActionsPermissions.Actions } + } + ); + yield return ( null, diff --git a/source/Nuke.Common.Tests/Nuke.Common.Tests.csproj b/source/Nuke.Common.Tests/Nuke.Common.Tests.csproj index 75b2a2fb8..fa2a29709 100644 --- a/source/Nuke.Common.Tests/Nuke.Common.Tests.csproj +++ b/source/Nuke.Common.Tests/Nuke.Common.Tests.csproj @@ -8,4 +8,11 @@ + + + $([System.String]::Copy('%(FileName)').Split('.')[0]) + %(ParentFile).cs + + + diff --git a/source/Nuke.Common/CI/GitHubActions/Configuration/GitHubActionsJob.cs b/source/Nuke.Common/CI/GitHubActions/Configuration/GitHubActionsJob.cs index 61dc3e4e8..7c9257bbe 100644 --- a/source/Nuke.Common/CI/GitHubActions/Configuration/GitHubActionsJob.cs +++ b/source/Nuke.Common/CI/GitHubActions/Configuration/GitHubActionsJob.cs @@ -1,4 +1,4 @@ -// Copyright 2023 Maintainers of NUKE. +// Copyright 2023 Maintainers of NUKE. // Distributed under the MIT License. // https://github.com/nuke-build/nuke/blob/master/LICENSE @@ -16,6 +16,8 @@ public class GitHubActionsJob : ConfigurationEntity { public string Name { get; set; } public GitHubActionsImage Image { get; set; } + public string[] CustomRunnerLabels { get; set; } = []; + public string CustomRunnerGroup { get; set; } public int TimeoutMinutes { get; set; } public string ConcurrencyGroup { get; set; } public bool ConcurrencyCancelInProgress { get; set; } @@ -28,7 +30,8 @@ public override void Write(CustomFileWriter writer) using (writer.Indent()) { writer.WriteLine($"name: {Name}"); - writer.WriteLine($"runs-on: {Image.GetValue()}"); + + WriteRunsOn(writer); if (TimeoutMinutes > 0) { @@ -63,4 +66,36 @@ public override void Write(CustomFileWriter writer) } } } + + private void WriteRunsOn(CustomFileWriter writer) + { + if (Image != GitHubActionsImage.SelfHosted) + { + writer.WriteLine($"runs-on: {Image.GetValue()}"); + return; + } + + if (!CustomRunnerGroup.IsNullOrWhiteSpace()) + { + writer.WriteLine($"runs-on:"); + + using (writer.Indent()) + { + writer.WriteLine($"group: {CustomRunnerGroup}"); + + if (CustomRunnerLabels.Length > 0) + { + writer.WriteInlineArray("labels", [Image.GetValue(), .. CustomRunnerLabels]); + } + } + } + else if (CustomRunnerLabels.Length > 0) + { + writer.WriteInlineArray("runs-on", [Image.GetValue(), .. CustomRunnerLabels]); + } + else + { + writer.WriteLine($"runs-on: {Image.GetValue()}"); + } + } } diff --git a/source/Nuke.Common/CI/GitHubActions/Configuration/GithubActionsCustomWriterExtensions.cs b/source/Nuke.Common/CI/GitHubActions/Configuration/GithubActionsCustomWriterExtensions.cs new file mode 100644 index 000000000..06e9642ea --- /dev/null +++ b/source/Nuke.Common/CI/GitHubActions/Configuration/GithubActionsCustomWriterExtensions.cs @@ -0,0 +1,28 @@ +// Copyright 2023 Maintainers of NUKE. +// Distributed under the MIT License. +// https://github.com/nuke-build/nuke/blob/master/LICENSE + +using System; +using System.Linq; +using Nuke.Common.Utilities; + +namespace Nuke.Common.CI.GitHubActions.Configuration; + +public static class GithubActionsCustomWriterExtensions +{ + public static void WriteInlineArray(this CustomFileWriter writer, string property, string[] values) + { + if (values.Length == 0) + { + return; + } + + if (values.Length <= 1) + { + writer.WriteLine($"{property}: {values.Single()}"); + return; + } + + writer.WriteLine($"{property}: [ {values.JoinCommaSpace()} ]"); + } +} diff --git a/source/Nuke.Common/CI/GitHubActions/GitHubActionsAttribute.cs b/source/Nuke.Common/CI/GitHubActions/GitHubActionsAttribute.cs index 497702c69..374feae34 100644 --- a/source/Nuke.Common/CI/GitHubActions/GitHubActionsAttribute.cs +++ b/source/Nuke.Common/CI/GitHubActions/GitHubActionsAttribute.cs @@ -80,6 +80,9 @@ public GitHubActionsAttribute( public string[] InvokedTargets { get; set; } = new string[0]; + public string[] CustomRunnerLabels { get; set; } = []; + public string CustomRunnerGroup { get; set; } + public GitHubActionsSubmodules Submodules { set => _submodules = value; @@ -130,6 +133,8 @@ protected virtual GitHubActionsJob GetJobs(GitHubActionsImage image, IReadOnlyCo Name = image.GetValue().Replace(".", "_"), Steps = GetSteps(image, relevantTargets).ToArray(), Image = image, + CustomRunnerGroup = CustomRunnerGroup, + CustomRunnerLabels = CustomRunnerLabels, TimeoutMinutes = TimeoutMinutes, ConcurrencyGroup = JobConcurrencyGroup, ConcurrencyCancelInProgress = JobConcurrencyCancelInProgress