diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 1f9ec1ce7..0e011f2cc 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,4 +1,4 @@ -image: mcr.microsoft.com/dotnet/sdk:5.0 +image: mcr.microsoft.com/dotnet/sdk:8.0 variables: GIT_DEPTH: 0 diff --git a/.space.kts b/.space.kts index 2fa4be911..492caaa86 100644 --- a/.space.kts +++ b/.space.kts @@ -22,7 +22,7 @@ job("continuous") { } } - container("mcr.microsoft.com/dotnet/sdk:6.0") { + container("mcr.microsoft.com/dotnet/sdk:8.0") { shellScript { content = "./build.sh Test" } diff --git a/Dockerfile b/Dockerfile index f9727a4d3..b5e9d6525 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,7 +1,7 @@ # docker build --no-cache --progress=plain -t my-image . ARG registryUrl -FROM mcr.microsoft.com/dotnet/sdk:6.0 AS builder +FROM mcr.microsoft.com/dotnet/sdk:8.0 AS builder WORKDIR / COPY ./ /code diff --git a/build.ps1 b/build.ps1 index 723645491..3bbe0fa47 100644 --- a/build.ps1 +++ b/build.ps1 @@ -20,9 +20,8 @@ $DotNetGlobalFile = "$PSScriptRoot\global.json" $DotNetInstallUrl = "https://dot.net/v1/dotnet-install.ps1" $DotNetChannel = "STS" -$env:DOTNET_SKIP_FIRST_TIME_EXPERIENCE = 1 $env:DOTNET_CLI_TELEMETRY_OPTOUT = 1 -$env:DOTNET_MULTILEVEL_LOOKUP = 0 +$env:DOTNET_NOLOGO = 1 $env:DOTNET_ROLL_FORWARD = "Major" $env:NUKE_TELEMETRY_OPTOUT = 1 @@ -72,6 +71,7 @@ else { ExecSafe { & powershell $DotNetInstallFile -InstallDir $DotNetDirectory -Version $DotNetVersion -NoPath } } $env:DOTNET_EXE = "$DotNetDirectory\dotnet.exe" + $env:PATH = "$DotNetDirectory;$env:PATH" } Write-Output "Microsoft (R) .NET SDK version $(& $env:DOTNET_EXE --version)" diff --git a/build.sh b/build.sh index 053d6ecf6..441eebc6d 100755 --- a/build.sh +++ b/build.sh @@ -17,8 +17,7 @@ DOTNET_INSTALL_URL="https://dot.net/v1/dotnet-install.sh" DOTNET_CHANNEL="STS" export DOTNET_CLI_TELEMETRY_OPTOUT=1 -export DOTNET_SKIP_FIRST_TIME_EXPERIENCE=1 -export DOTNET_MULTILEVEL_LOOKUP=0 +export DOTNET_NOLOGO=1 export DOTNET_ROLL_FORWARD="Major" export NUKE_TELEMETRY_OPTOUT=1 @@ -65,6 +64,7 @@ else "$DOTNET_INSTALL_FILE" --install-dir "$DOTNET_DIRECTORY" --version "$DOTNET_VERSION" --no-path fi export DOTNET_EXE="$DOTNET_DIRECTORY/dotnet" + export PATH="$DOTNET_DIRECTORY:$PATH" fi echo "Microsoft (R) .NET SDK version $("$DOTNET_EXE" --version)" diff --git a/build/Build.CI.SpaceAutomation.cs b/build/Build.CI.SpaceAutomation.cs index 89cbd1c1c..8d9fffb78 100644 --- a/build/Build.CI.SpaceAutomation.cs +++ b/build/Build.CI.SpaceAutomation.cs @@ -7,7 +7,7 @@ [SpaceAutomation( name: "continuous", - image: "mcr.microsoft.com/dotnet/sdk:6.0", + image: "mcr.microsoft.com/dotnet/sdk:8.0", OnPush = true, InvokedTargets = new[] { nameof(ITest.Test) })] partial class Build diff --git a/build/_build.csproj b/build/_build.csproj index 33b125656..b62c736dc 100644 --- a/build/_build.csproj +++ b/build/_build.csproj @@ -4,7 +4,7 @@ Exe - net6.0 + net8.0 preview CS0649;CS0169 diff --git a/docs/03-common/03-paths.md b/docs/03-common/03-paths.md index 7d823b071..9b1ab19ad 100644 --- a/docs/03-common/03-paths.md +++ b/docs/03-common/03-paths.md @@ -27,7 +27,8 @@ var extensionWithDot = IndexFile.Extension; // Get the parent directory var parent1 = IndexFile.Parent; -var parent2 = IndexFile / ".."; // gets normalized +var parent2 = IndexFile / ..; // gets normalized +var parent3 = IndexFile / ".."; // gets normalized // Check if one path contains another var containsFile = SourceDirectory.Contains(IndexFile); diff --git a/global.json b/global.json index 9c5310119..c19a2e057 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,6 @@ { "sdk": { - "version": "7.0.201", + "version": "8.0.100", "rollForward": "latestMinor" } } diff --git a/source/Directory.Build.props b/source/Directory.Build.props index 68f878cb8..11a271277 100644 --- a/source/Directory.Build.props +++ b/source/Directory.Build.props @@ -3,11 +3,12 @@ preview true - CS1591;NU5129;NU5118 + CS1591;NU5129;NU5118;SYSLIB0050;SYSLIB0051 $(DefineConstants);JETBRAINS_ANNOTATIONS embedded true true + true diff --git a/source/Nuke.Build.Shared/Notifications.cs b/source/Nuke.Build.Shared/Notifications.cs new file mode 100644 index 000000000..83bc15c3a --- /dev/null +++ b/source/Nuke.Build.Shared/Notifications.cs @@ -0,0 +1,98 @@ +// 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 System.Net.Http; +using System.Text.Json; +using System.Threading.Tasks; +using JetBrains.Annotations; +using Nuke.Common; +using Nuke.Common.IO; +using Nuke.Common.Utilities; + +namespace Nuke.Build.Shared; + +[PublicAPI] +internal record Notification(string Title, string Text, Link[] Links) +{ + public string Title { get; } = Title; + public string Text { get; } = Text; + public Link[] Links { get; } = Links; +} + +[PublicAPI] +internal record Link(string Text, string Url) +{ + public string Text { get; } = Text; + public string Url { get; } = Url; +} + +[PublicAPI] +internal class NotificationFetcher +{ + private const string NotificationEndpoint = "https://nuke.build/notifications.json"; + private const string UtmMedium = "development"; + + private readonly AbsolutePath _notificationDirectory = Constants.GlobalNukeDirectory / "received-notifications"; + private readonly string _utmSource; + + public NotificationFetcher(string utmSource) + { + _utmSource = utmSource; + } + + public async Task GetNotificationAsync() + { + try + { + return await GetNotificationInternal(); + } + catch (Exception) + { + return null; + } + } + + private async Task GetNotificationInternal() + { + var httpClient = new HttpClient(); + var response = await httpClient.GetAsync(NotificationEndpoint); + if (!response.IsSuccessStatusCode) + return null; + + var content = await response.Content.ReadAsStringAsync(); + var json = JsonDocument.Parse(content).RootElement; + + var notification = json.EnumerateArray() + .Where(IsApplicable) + .Select(x => (Json: x, File: _notificationDirectory / x.ToString().GetMD5Hash())) + .FirstOrDefault(x => !x.File.Exists()); + if (notification.File == null) + return null; + + notification.File.TouchFile(); + + return new Notification( + Title: notification.Json.GetProperty("title").GetString(), + Text: notification.Json.GetProperty("text").GetString(), + Links: notification.Json.GetProperty("links").EnumerateArray().Select(GetLink).ToArray()); + + bool IsApplicable(JsonElement element) + => !element.TryGetProperty("exclude", out var exclusions) || + !exclusions.EnumerateArray().Select(x => x.GetString()).Contains(_utmSource); + + Link GetLink(JsonElement obj) + { + var originalUrl = new Uri(obj.GetProperty("url").GetString().NotNullOrEmpty()) + .WithUtmValues( + medium: UtmMedium, + source: _utmSource, + campaign: obj.GetProperty("campaign").GetString()); + return new Link( + Text: obj.GetProperty("title").GetString(), + Url: originalUrl.AbsoluteUri); + } + } +} diff --git a/source/Nuke.Build.Tests/BuildExecutorTest.cs b/source/Nuke.Build.Tests/BuildExecutorTest.cs index 62f4fcf08..d7c89899d 100644 --- a/source/Nuke.Build.Tests/BuildExecutorTest.cs +++ b/source/Nuke.Build.Tests/BuildExecutorTest.cs @@ -47,8 +47,8 @@ public void TestParameterSkipped_AllWithInvoked() { C.Invoked = true; ExecuteBuild(skippedTargets: new ExecutableTarget[0]); - AssertSucceeded(C); - AssertSkipped(A, B); + AssertSucceeded(); + AssertSkipped(A, B, C); } [Fact] @@ -108,6 +108,27 @@ public void TestStaticCondition_DependencyBehavior_Skip() B.OnlyWhen.Should().Be("false"); } + [Fact] + public void TestStaticCondition_Invoked_DependencyBehavior_Skip() + { + C.StaticConditions.Add(("() => false", () => false)); + C.DependencyBehavior = DependencyBehavior.Skip; + C.Invoked = true; + ExecuteBuild(); + AssertSkipped(A, B, C); + } + + [Fact] + public void TestStaticCondition_Invoked_DependencyBehavior_Execute() + { + C.StaticConditions.Add(("() => false", () => false)); + C.DependencyBehavior = DependencyBehavior.Execute; + C.Invoked = true; + ExecuteBuild(); + AssertSkipped(C); + AssertSucceeded(A, B); + } + [Fact] public void TestStaticCondition_Multiple() { diff --git a/source/Nuke.Build/Execution/BuildExecutor.cs b/source/Nuke.Build/Execution/BuildExecutor.cs index 5d6b50e44..11892eb48 100644 --- a/source/Nuke.Build/Execution/BuildExecutor.cs +++ b/source/Nuke.Build/Execution/BuildExecutor.cs @@ -180,7 +180,7 @@ string Format(string condition) private static void MarkTargetSkipped(INukeBuild build, ExecutableTarget target, string reason = null) { - if (target.Invoked || target.Status != ExecutionStatus.Scheduled) + if (target.Status != ExecutionStatus.Scheduled) return; target.Status = ExecutionStatus.Skipped; diff --git a/source/Nuke.Build/Execution/DelegateRequirementService.cs b/source/Nuke.Build/Execution/DelegateRequirementService.cs index 641a6117e..851b8834a 100644 --- a/source/Nuke.Build/Execution/DelegateRequirementService.cs +++ b/source/Nuke.Build/Execution/DelegateRequirementService.cs @@ -25,8 +25,8 @@ public static void ValidateRequirements(INukeBuild build, IReadOnlyCollection> boolExpression) // TODO: same as HasSkippingCondition.GetSkipReason Assert.True(boolExpression.Compile().Invoke(), $"Target '{target.Name}' requires '{requirement.Body}'"); - else if (IsMemberNull(requirement.GetMemberInfo(), build, target)) - Assert.Fail($"Target '{target.Name}' requires member '{GetMemberName(requirement.GetMemberInfo())}' to be not null"); + else if (IsMemberNullOrEmpty(requirement.GetMemberInfo(), build, target)) + Assert.Fail($"Target '{target.Name}' requires member '{GetMemberName(requirement.GetMemberInfo())}' to be not null or empty"); } var requiredMembers = ValueInjectionUtility.GetInjectionMembers(build.GetType()) @@ -34,12 +34,12 @@ public static void ValidateRequirements(INukeBuild build, IReadOnlyCollection x.HasCustomAttribute()); foreach (var member in requiredMembers) { - if (IsMemberNull(member, build)) - Assert.Fail($"Member '{GetMemberName(member)}' is required to be not null"); + if (IsMemberNullOrEmpty(member, build)) + Assert.Fail($"Member '{GetMemberName(member)}' is required to be not null or empty"); } } - private static bool IsMemberNull(MemberInfo member, INukeBuild build, ExecutableTarget target = null) + private static bool IsMemberNullOrEmpty(MemberInfo member, INukeBuild build, ExecutableTarget target = null) { member = member.DeclaringType != build.GetType() ? build.GetType().GetMember(member.Name).SingleOrDefault() ?? member @@ -52,7 +52,9 @@ private static bool IsMemberNull(MemberInfo member, INukeBuild build, Executable if (build.Host is Terminal) TryInjectValueInteractive(member, build); - return member.GetValue(build) == null; + return member.GetMemberType() != typeof(string) + ? member.GetValue(build) == null + : member.GetValue(build).IsNullOrWhiteSpace(); } private static void TryInjectValueInteractive(MemberInfo member, INukeBuild build) diff --git a/source/Nuke.Build/Execution/ToolRequirementService.cs b/source/Nuke.Build/Execution/ToolRequirementService.cs index 49f2c7c8d..b371b5dc7 100644 --- a/source/Nuke.Build/Execution/ToolRequirementService.cs +++ b/source/Nuke.Build/Execution/ToolRequirementService.cs @@ -42,7 +42,7 @@ private static void InstallNuGetPackages(IReadOnlyCollection - net6.0 + net8.0 diff --git a/source/Nuke.Common/Nuke.Common.props b/source/Nuke.Common/Nuke.Common.props index d41fceeda..9827b4735 100644 --- a/source/Nuke.Common/Nuke.Common.props +++ b/source/Nuke.Common/Nuke.Common.props @@ -4,6 +4,8 @@ false $(MSBuildWarningsAsErrors);CS8785 + $(NoWarn);SYSLIB0050;SYSLIB0051 + true diff --git a/source/Nuke.Common/Tools/SonarScanner/SonarScanner.json b/source/Nuke.Common/Tools/SonarScanner/SonarScanner.json index f66f650a4..010f7913b 100644 --- a/source/Nuke.Common/Tools/SonarScanner/SonarScanner.json +++ b/source/Nuke.Common/Tools/SonarScanner/SonarScanner.json @@ -77,6 +77,7 @@ "name": "Verbose", "type": "bool", "format": "/d:sonar.verbose={value}", + "customValue": true, "help": "Sets the logging verbosity to detailed. Add this argument before sending logs for troubleshooting." }, { diff --git a/source/Nuke.GlobalTool.Tests/cake-scripts/paths.verified.cs b/source/Nuke.GlobalTool.Tests/cake-scripts/paths.verified.cs index 9de732bb4..2406c7d8f 100644 --- a/source/Nuke.GlobalTool.Tests/cake-scripts/paths.verified.cs +++ b/source/Nuke.GlobalTool.Tests/cake-scripts/paths.verified.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.IO; using System.Linq; @@ -40,9 +40,13 @@ class Build : NukeBuild { AbsolutePath LocalPackagesDir => RootDirectory / ".." / "LocalPackages"; + AbsolutePath SourceFolder => RootDirectory / "source"; + AbsolutePath PublishDir => RootDirectory / "publish"; + AbsolutePath SignToolPath => RootDirectory / "certificates" / "signtool.exe"; + private string Convert(AbsolutePath file) { file = (AbsolutePath)file; diff --git a/source/Nuke.GlobalTool.Tests/cake-scripts/targets.verified.cs b/source/Nuke.GlobalTool.Tests/cake-scripts/targets.verified.cs index 4978b2faa..be8919a8e 100644 --- a/source/Nuke.GlobalTool.Tests/cake-scripts/targets.verified.cs +++ b/source/Nuke.GlobalTool.Tests/cake-scripts/targets.verified.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.IO; using System.Linq; @@ -46,6 +46,7 @@ class Build : NukeBuild System.Console.WriteLine(); }); + Target B => _ => _ .DependsOn(A) .DependentFor(A) @@ -54,6 +55,7 @@ class Build : NukeBuild System.Console.WriteLine(); }); + Target C_1 => _ => _ .DependsOn(B) .OnlyWhenStatic(() => staticCondition) diff --git a/source/Nuke.GlobalTool/Program.Setup.cs b/source/Nuke.GlobalTool/Program.Setup.cs index dcffc167e..4804be675 100644 --- a/source/Nuke.GlobalTool/Program.Setup.cs +++ b/source/Nuke.GlobalTool/Program.Setup.cs @@ -27,7 +27,7 @@ partial class Program { // ReSharper disable InconsistentNaming - private const string TARGET_FRAMEWORK = "net6.0"; + private const string TARGET_FRAMEWORK = "net8.0"; private const string PROJECT_KIND = "9A19103F-16F7-4668-BE54-9A1E7A4F7556"; // ReSharper disable once CognitiveComplexity diff --git a/source/Nuke.GlobalTool/ProjectUpdater.cs b/source/Nuke.GlobalTool/ProjectUpdater.cs index 9cb7487f5..9bdd4a8be 100644 --- a/source/Nuke.GlobalTool/ProjectUpdater.cs +++ b/source/Nuke.GlobalTool/ProjectUpdater.cs @@ -29,7 +29,7 @@ public static void Update(string projectFile) private static void UpdateTargetFramework(Microsoft.Build.Evaluation.Project buildProject) { - buildProject.SetProperty("TargetFramework", "net6.0"); + buildProject.SetProperty("TargetFramework", "net8.0"); } private static void UpdateNukeCommonPackage(Microsoft.Build.Evaluation.Project buildProject, out FloatRange previousPackageVersion) diff --git a/source/Nuke.GlobalTool/templates/build.ps1 b/source/Nuke.GlobalTool/templates/build.ps1 index 25f8f3e07..712e2f89f 100644 --- a/source/Nuke.GlobalTool/templates/build.ps1 +++ b/source/Nuke.GlobalTool/templates/build.ps1 @@ -20,9 +20,8 @@ $DotNetGlobalFile = "$PSScriptRoot\_ROOT_DIRECTORY_\global.json" $DotNetInstallUrl = "https://dot.net/v1/dotnet-install.ps1" $DotNetChannel = "STS" -$env:DOTNET_SKIP_FIRST_TIME_EXPERIENCE = 1 $env:DOTNET_CLI_TELEMETRY_OPTOUT = 1 -$env:DOTNET_MULTILEVEL_LOOKUP = 0 +$env:DOTNET_NOLOGO = 1 ########################################################################### # EXECUTION @@ -61,6 +60,7 @@ else { ExecSafe { & powershell $DotNetInstallFile -InstallDir $DotNetDirectory -Version $DotNetVersion -NoPath } } $env:DOTNET_EXE = "$DotNetDirectory\dotnet.exe" + $env:PATH = "$DotNetDirectory;$env:PATH" } Write-Output "Microsoft (R) .NET SDK version $(& $env:DOTNET_EXE --version)" diff --git a/source/Nuke.GlobalTool/templates/build.sh b/source/Nuke.GlobalTool/templates/build.sh index 9f459d316..bdee09282 100644 --- a/source/Nuke.GlobalTool/templates/build.sh +++ b/source/Nuke.GlobalTool/templates/build.sh @@ -17,8 +17,7 @@ DOTNET_INSTALL_URL="https://dot.net/v1/dotnet-install.sh" DOTNET_CHANNEL="STS" export DOTNET_CLI_TELEMETRY_OPTOUT=1 -export DOTNET_SKIP_FIRST_TIME_EXPERIENCE=1 -export DOTNET_MULTILEVEL_LOOKUP=0 +export DOTNET_NOLOGO=1 ########################################################################### # EXECUTION @@ -54,6 +53,7 @@ else "$DOTNET_INSTALL_FILE" --install-dir "$DOTNET_DIRECTORY" --version "$DOTNET_VERSION" --no-path fi export DOTNET_EXE="$DOTNET_DIRECTORY/dotnet" + export PATH="$DOTNET_DIRECTORY:$PATH" fi echo "Microsoft (R) .NET SDK version $("$DOTNET_EXE" --version)" diff --git a/source/Nuke.ProjectModel.Tests/ProjectModelTest.cs b/source/Nuke.ProjectModel.Tests/ProjectModelTest.cs index 855a37856..11b67ba1b 100644 --- a/source/Nuke.ProjectModel.Tests/ProjectModelTest.cs +++ b/source/Nuke.ProjectModel.Tests/ProjectModelTest.cs @@ -26,7 +26,7 @@ public void ProjectTest() var action = new Action(() => project.GetMSBuildProject()); action.Should().NotThrow(); - project.GetTargetFrameworks().Should().Equal("net6.0", "net7.0"); + project.GetTargetFrameworks().Should().Equal("net6.0", "net7.0", "net8.0"); project.HasPackageReference("Microsoft.Build.Locator").Should().BeTrue(); project.GetPackageReferenceVersion("Microsoft.Build.Locator").Should().Be("1.6.10"); } diff --git a/source/Nuke.ProjectModel/Nuke.ProjectModel.csproj b/source/Nuke.ProjectModel/Nuke.ProjectModel.csproj index 95c8a1334..923c081a6 100644 --- a/source/Nuke.ProjectModel/Nuke.ProjectModel.csproj +++ b/source/Nuke.ProjectModel/Nuke.ProjectModel.csproj @@ -1,7 +1,7 @@ - net6.0;net7.0 + net6.0;net7.0;net8.0 @@ -11,7 +11,14 @@ - + + + + + + + + diff --git a/source/Nuke.SourceGenerators.Tests/StronglyTypedSolutionGeneratorTest.Test#StronglyTypedSolutionGenerator.verified.cs b/source/Nuke.SourceGenerators.Tests/StronglyTypedSolutionGeneratorTest.Test#StronglyTypedSolutionGenerator.verified.cs index a860a8cda..a72c5367f 100644 --- a/source/Nuke.SourceGenerators.Tests/StronglyTypedSolutionGeneratorTest.Test#StronglyTypedSolutionGenerator.verified.cs +++ b/source/Nuke.SourceGenerators.Tests/StronglyTypedSolutionGeneratorTest.Test#StronglyTypedSolutionGenerator.verified.cs @@ -32,10 +32,11 @@ internal class Solution : Nuke.Common.ProjectModel.Solution public Project Nuke_Utilities_Text_Yaml => SolutionFolder.GetProject("Nuke.Utilities.Text.Yaml"); public Project Nuke_Utilities_IO_Compression => SolutionFolder.GetProject("Nuke.Utilities.IO.Compression"); public _misc misc => new(SolutionFolder.GetSolutionFolder("misc")); + internal class _misc { private SolutionFolder SolutionFolder { get; } public _misc(SolutionFolder solutionFolder) => SolutionFolder = solutionFolder; } -} +} \ No newline at end of file diff --git a/source/Nuke.Tooling/IProcess.cs b/source/Nuke.Tooling/IProcess.cs index 365d26b53..d38f87e7e 100644 --- a/source/Nuke.Tooling/IProcess.cs +++ b/source/Nuke.Tooling/IProcess.cs @@ -57,7 +57,16 @@ public interface IProcess : IDisposable void Kill(); /// - /// Waits for the process to exit. If the process is not exiting within a given timeout, is called. + /// Calls platform specific code to kill entire process tree. + /// + /// + /// For Windows it call "taskkill /T /F /PID {process.Id}". + /// For Unix it call combination of pgrep and kill commands + /// + void KillTree(); + + /// + /// Waits for the process to exit. If the process is not exiting within a given timeout, is called. /// /// /// Returns true, if the process exited on its own. diff --git a/source/Nuke.Tooling/NativeProcessExtension.cs b/source/Nuke.Tooling/NativeProcessExtension.cs new file mode 100644 index 000000000..920ed986e --- /dev/null +++ b/source/Nuke.Tooling/NativeProcessExtension.cs @@ -0,0 +1,114 @@ +// Copyright 2023 Maintainers of NUKE. +// Distributed under the MIT License. +// https://github.com/nuke-build/nuke/blob/master/LICENSE + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Runtime.InteropServices; + +namespace Nuke.Common.Tooling; + +// https://raw.githubusercontent.com/dotnet/cli/master/test/Microsoft.DotNet.Tools.Tests.Utilities/Extensions/ProcessExtensions.cs +internal static class NativeProcessExtension +{ + private static readonly bool _isWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows); + private static readonly TimeSpan _defaultTimeout = TimeSpan.FromSeconds(30); + + internal static void KillTree(this Process process) + { + process.KillTree(_defaultTimeout); + } + + internal static void KillTree(this Process process, TimeSpan timeout) + { + string stdout; + if (_isWindows) + { + RunProcessAndWaitForExit( + "taskkill", + $"/T /F /PID {process.Id}", + timeout, + out stdout); + } + else + { + var children = new HashSet(); + GetAllChildIdsUnix(process.Id, children, timeout); + foreach (var childId in children) + { + KillProcessUnix(childId, timeout); + } + KillProcessUnix(process.Id, timeout); + } + } + + private static void GetAllChildIdsUnix(int parentId, ISet children, TimeSpan timeout) + { + string stdout; + var exitCode = RunProcessAndWaitForExit( + "pgrep", + $"-P {parentId}", + timeout, + out stdout); + + if (exitCode == 0 && !string.IsNullOrEmpty(stdout)) + { + using (var reader = new StringReader(stdout)) + { + while (true) + { + var text = reader.ReadLine(); + if (text == null) + { + return; + } + + int id; + if (int.TryParse(text, out id)) + { + children.Add(id); + // Recursively get the children + GetAllChildIdsUnix(id, children, timeout); + } + } + } + } + } + + private static void KillProcessUnix(int processId, TimeSpan timeout) + { + string stdout; + RunProcessAndWaitForExit( + "kill", + $"-TERM {processId}", + timeout, + out stdout); + } + + private static int RunProcessAndWaitForExit(string fileName, string arguments, TimeSpan timeout, out string stdout) + { + var startInfo = new ProcessStartInfo + { + FileName = fileName, + Arguments = arguments, + RedirectStandardOutput = true, + UseShellExecute = false + }; + + var process = Process.Start(startInfo); + + stdout = null; + if (process.WaitForExit((int)timeout.TotalMilliseconds)) + { + stdout = process.StandardOutput.ReadToEnd(); + } + else + { + process.Kill(); + } + + return process.ExitCode; + } +} diff --git a/source/Nuke.Tooling/Process2.cs b/source/Nuke.Tooling/Process2.cs index fb3f1f8ca..56a2eaeb0 100644 --- a/source/Nuke.Tooling/Process2.cs +++ b/source/Nuke.Tooling/Process2.cs @@ -44,7 +44,12 @@ public void Dispose() public void Kill() { - _process.Kill(); + _process.Kill(); + } + + public void KillTree() + { + _process.KillTree(); } public bool WaitForExit() @@ -53,7 +58,7 @@ public bool WaitForExit() // use _process.StartTime var hasExited = _process.WaitForExit(_timeout ?? -1); if (!hasExited) - _process.Kill(); + _process.KillTree(); return hasExited; } } diff --git a/source/Nuke.Tooling/ToolingExtensions.cs b/source/Nuke.Tooling/ToolingExtensions.cs index 44c8b8b9c..86ae0f568 100644 --- a/source/Nuke.Tooling/ToolingExtensions.cs +++ b/source/Nuke.Tooling/ToolingExtensions.cs @@ -6,7 +6,10 @@ using System.Linq; using JetBrains.Annotations; using Nuke.Common.IO; +using Nuke.Common.Utilities; using Nuke.Common.Utilities.Collections; +using Serilog; +using Serilog.Events; namespace Nuke.Common.Tooling; @@ -65,4 +68,13 @@ public static void Open(this AbsolutePath path) var verb = EnvironmentInfo.IsUnix ? "open" : path.DirectoryExists() ? "explorer.exe" : "call"; ProcessTasks.StartShell($"{verb} {path}"); } + + /// + /// Prints the content of a file using the specified . + /// + public static AbsolutePath Print(this AbsolutePath path, LogEventLevel level = LogEventLevel.Information) + { + Log.Write(level, "Content of {Path}".Append(Environment.NewLine).Append(path.ReadAllText()), path); + return path; + } } diff --git a/source/Nuke.Utilities.Tests/IO/PathConstructionTest.cs b/source/Nuke.Utilities.Tests/IO/PathConstructionTest.cs index c8a9c70ed..7c1d024ff 100644 --- a/source/Nuke.Utilities.Tests/IO/PathConstructionTest.cs +++ b/source/Nuke.Utilities.Tests/IO/PathConstructionTest.cs @@ -24,6 +24,7 @@ public void TestParent(string path, string expected) { ((AbsolutePath) path).Parent.Should().Be((AbsolutePath) expected); ((string) ((AbsolutePath) path).Parent).Should().Be(expected); + ((AbsolutePath)path / ..).Should().Be((AbsolutePath)expected); } [Theory] @@ -214,6 +215,14 @@ public void RelativePath_Specific() ((string) (WinRelativePath) "foo/bar").Should().Be("foo\\bar"); } + [Fact] + public void RelativePath_Parent() + { + ((string) ((UnixRelativePath) "foo/bar/foo" / ..)).Should().Be("foo/bar"); + ((string) ((WinRelativePath) "foo/bar/foo" / ..)).Should().Be("foo\\bar"); + ((string) ((RelativePath) "foo/bar" / ..)).Should().Be("foo"); + } + private static string ParseRelativePath(object[] parts) { return parts.Skip(count: 1).Aggregate((RelativePath) (string) parts[0], (rp, p) => rp / (string) p); diff --git a/source/Nuke.Utilities/Collections/Enumerable.Random.cs b/source/Nuke.Utilities/Collections/Enumerable.Random.cs new file mode 100644 index 000000000..c38d28ae7 --- /dev/null +++ b/source/Nuke.Utilities/Collections/Enumerable.Random.cs @@ -0,0 +1,33 @@ +// Copyright 2023 Maintainers of NUKE. +// Distributed under the MIT License. +// https://github.com/nuke-build/nuke/blob/master/LICENSE + +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Nuke.Common.Utilities.Collections; + +partial class EnumerableExtensions +{ + private static readonly Random s_randomNumberGenerator = new Random(); + + public static T Random(this IEnumerable collection) + { + var array = collection.ToArray(); + return array[s_randomNumberGenerator.Next(array.Length)]; + } + + public static ICollection Randomize(this ICollection collection) + { + var list = collection.ToList(); + var count = list.Count; + while (count > 1) { + count--; + var k = s_randomNumberGenerator.Next(count + 1); + (list[k], list[count]) = (list[count], list[k]); + } + + return list; + } +} diff --git a/source/Nuke.Utilities/Exception.Unwrap.cs b/source/Nuke.Utilities/Exception.Unwrap.cs index 22f300e60..fbca171bb 100644 --- a/source/Nuke.Utilities/Exception.Unwrap.cs +++ b/source/Nuke.Utilities/Exception.Unwrap.cs @@ -3,10 +3,15 @@ // https://github.com/nuke-build/nuke/blob/master/LICENSE using System; +using System.Diagnostics; using System.Reflection; +using JetBrains.Annotations; namespace Nuke.Common.Utilities; +[PublicAPI] +[DebuggerNonUserCode] +[DebuggerStepThrough] public static class ExceptionExtensions { /// diff --git a/source/Nuke.Utilities/IO/AbsolutePath.cs b/source/Nuke.Utilities/IO/AbsolutePath.cs index f0c5aa4bc..e92da98f5 100644 --- a/source/Nuke.Utilities/IO/AbsolutePath.cs +++ b/source/Nuke.Utilities/IO/AbsolutePath.cs @@ -106,6 +106,16 @@ public static implicit operator string([CanBeNull] AbsolutePath path) ? this / ".." : null; +#if NET6_0_OR_GREATER + + public static AbsolutePath operator /(AbsolutePath left, [CanBeNull] Range range) + { + Assert.True(range.Equals(Range.All)); + return left.Parent; + } + +#endif + public static AbsolutePath operator /(AbsolutePath left, [CanBeNull] string right) { return new AbsolutePath(Combine(left.NotNull(), right)); diff --git a/source/Nuke.Utilities/IO/RelativePath.cs b/source/Nuke.Utilities/IO/RelativePath.cs index f2763d377..5191c8922 100644 --- a/source/Nuke.Utilities/IO/RelativePath.cs +++ b/source/Nuke.Utilities/IO/RelativePath.cs @@ -40,12 +40,27 @@ public static implicit operator string([CanBeNull] RelativePath path) return path?._path; } +#if NET6_0_OR_GREATER + + public static RelativePath operator /(RelativePath left, [CanBeNull] Range range) + { + Assert.True(range.Equals(Range.All)); + return left / ".."; + } + +#endif + public static RelativePath operator /(RelativePath left, [CanBeNull] string right) { var separator = left.NotNull()._separator; return new RelativePath(NormalizePath(Combine(left, (RelativePath) right, separator), separator), separator); } + public static RelativePath operator +(RelativePath left, [CanBeNull] string right) + { + return new RelativePath(left.ToString() + right); + } + public override string ToString() { return _path; diff --git a/source/Nuke.Utilities/Lazy.cs b/source/Nuke.Utilities/Lazy.cs index 019ff66f2..8543f1863 100644 --- a/source/Nuke.Utilities/Lazy.cs +++ b/source/Nuke.Utilities/Lazy.cs @@ -3,9 +3,15 @@ // https://github.com/nuke-build/nuke/blob/master/LICENSE using System; +using System.Diagnostics; +using System.Linq; +using JetBrains.Annotations; namespace Nuke.Common.Utilities; +[PublicAPI] +[DebuggerNonUserCode] +[DebuggerStepThrough] public static class Lazy { /// diff --git a/source/Nuke.Utilities/Object.Apply.cs b/source/Nuke.Utilities/Object.Apply.cs new file mode 100644 index 000000000..ed5f1722e --- /dev/null +++ b/source/Nuke.Utilities/Object.Apply.cs @@ -0,0 +1,16 @@ +// Copyright 2023 Maintainers of NUKE. +// Distributed under the MIT License. +// https://github.com/nuke-build/nuke/blob/master/LICENSE + +using System; +using System.Linq; + +namespace Nuke.Common.Utilities; + +partial class ObjectExtensions +{ + public static TOutput Apply(this TInput input, Func transform) + { + return transform.Invoke(input); + } +} diff --git a/source/Nuke.Utilities/Object.Clone.cs b/source/Nuke.Utilities/Object.Clone.cs index e5456a9fe..341c73c12 100644 --- a/source/Nuke.Utilities/Object.Clone.cs +++ b/source/Nuke.Utilities/Object.Clone.cs @@ -14,7 +14,7 @@ namespace Nuke.Common.Utilities; [PublicAPI] [DebuggerNonUserCode] [DebuggerStepThrough] -public static class ObjectExtensions +public static partial class ObjectExtensions { /// /// Clones an object via . @@ -27,4 +27,4 @@ public static T Clone(this T obj) memoryStream.Seek(offset: 0, loc: SeekOrigin.Begin); return (T) serializer.ReadObject(memoryStream); } -} \ No newline at end of file +} diff --git a/source/Nuke.Utilities/Task.WaitAll.cs b/source/Nuke.Utilities/Task.WaitAll.cs new file mode 100644 index 000000000..6ca6b3215 --- /dev/null +++ b/source/Nuke.Utilities/Task.WaitAll.cs @@ -0,0 +1,31 @@ +// Copyright 2023 Maintainers of NUKE. +// Distributed under the MIT License. +// https://github.com/nuke-build/nuke/blob/master/LICENSE + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Threading.Tasks; +using JetBrains.Annotations; + +namespace Nuke.Common.Utilities; + +[PublicAPI] +[DebuggerNonUserCode] +[DebuggerStepThrough] +public static partial class TaskExtensions +{ + public static void WaitAll(this IEnumerable tasks) + { + var tasksArray = tasks.ToArray(); + Task.WaitAll(tasksArray); + } + + public static IReadOnlyCollection WaitAll(this IEnumerable> tasks) + { + var tasksArray = tasks.ToArray(); + Task.WaitAll(tasksArray); + return tasksArray.Select(x => x.Result).ToList(); + } +} diff --git a/source/Nuke.Utilities/Text/String.Truncate.cs b/source/Nuke.Utilities/Text/String.Truncate.cs new file mode 100644 index 000000000..c35207631 --- /dev/null +++ b/source/Nuke.Utilities/Text/String.Truncate.cs @@ -0,0 +1,16 @@ +// Copyright 2023 Maintainers of NUKE. +// Distributed under the MIT License. +// https://github.com/nuke-build/nuke/blob/master/LICENSE + +using System; +using System.Linq; + +namespace Nuke.Common.Utilities; + +partial class StringExtensions +{ + public static string Truncate(this string str, int maxChars) + { + return str.Length <= maxChars ? str : str.Substring(0, maxChars) + "…"; + } +} diff --git a/source/Nuke.Utilities/Url.WithUtmValues.cs b/source/Nuke.Utilities/Url.WithUtmValues.cs new file mode 100644 index 000000000..5748533b9 --- /dev/null +++ b/source/Nuke.Utilities/Url.WithUtmValues.cs @@ -0,0 +1,33 @@ +// Copyright 2023 Maintainers of NUKE. +// Distributed under the MIT License. +// https://github.com/nuke-build/nuke/blob/master/LICENSE + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using JetBrains.Annotations; + +namespace Nuke.Common.Utilities; + +[PublicAPI] +[DebuggerNonUserCode] +[DebuggerStepThrough] +public static class UrlExtensions +{ + public static Uri WithUtmValues(this Uri uri, string medium, string source, string campaign = null, string content = null) + { + var lastSegment = uri.Segments.Last().Trim('/').Apply(x => x.IsNullOrWhiteSpace() ? null : x); + + var dictionary = new Dictionary + { + ["utm_medium"] = medium.NotNullOrWhiteSpace(), + ["utm_source"] = source.NotNullOrWhiteSpace(), + ["utm_campaign"] = campaign ?? lastSegment, + ["utm_content"] = content ?? (campaign != null ? lastSegment : null), + }; + + var query = dictionary.Where(x => x.Value != null).Select(x => $"{x.Key}={x.Value}").Join("&"); + return new Uri(uri.AbsoluteUri + "?" + query); + } +}