diff --git a/.nuke/build.schema.json b/.nuke/build.schema.json index b42ce3ca0..719ef126d 100644 --- a/.nuke/build.schema.json +++ b/.nuke/build.schema.json @@ -1,76 +1,191 @@ { "$schema": "http://json-schema.org/draft-04/schema#", - "$ref": "#/definitions/build", - "title": "Build Schema", + "properties": { + "AutoStash": { + "type": "boolean" + }, + "CodecovToken": { + "type": "string", + "default": "Secrets must be entered via 'nuke :secrets [profile]'" + }, + "Configuration": { + "type": "string", + "enum": [ + "Debug", + "Release" + ] + }, + "DiscordWebhook": { + "type": "string", + "default": "Secrets must be entered via 'nuke :secrets [profile]'" + }, + "FeedzNuGetApiKey": { + "type": "string", + "default": "Secrets must be entered via 'nuke :secrets [profile]'" + }, + "GitHubReleaseGitHubToken": { + "type": "string", + "default": "Secrets must be entered via 'nuke :secrets [profile]'" + }, + "IgnoreFailedSources": { + "type": "boolean", + "description": "Ignore unreachable sources during Restore" + }, + "Major": { + "type": "boolean" + }, + "MastodonAccessToken": { + "type": "string", + "default": "Secrets must be entered via 'nuke :secrets [profile]'" + }, + "PublicNuGetApiKey": { + "type": "string", + "default": "Secrets must be entered via 'nuke :secrets [profile]'" + }, + "SignPathApiToken": { + "type": "string", + "default": "Secrets must be entered via 'nuke :secrets [profile]'" + }, + "SignPathSettings": { + "$ref": "#/definitions/SignPathSettings" + }, + "SlackWebhook": { + "type": "string", + "default": "Secrets must be entered via 'nuke :secrets [profile]'" + }, + "Solution": { + "type": "string", + "description": "Path to a solution file that is automatically loaded" + }, + "TestDegreeOfParallelism": { + "type": "integer", + "format": "int32" + }, + "TwitterAccessToken": { + "type": "string", + "default": "Secrets must be entered via 'nuke :secrets [profile]'" + }, + "TwitterAccessTokenSecret": { + "type": "string", + "default": "Secrets must be entered via 'nuke :secrets [profile]'" + }, + "TwitterConsumerKey": { + "type": "string", + "default": "Secrets must be entered via 'nuke :secrets [profile]'" + }, + "TwitterConsumerSecret": { + "type": "string", + "default": "Secrets must be entered via 'nuke :secrets [profile]'" + }, + "UseHttps": { + "type": "boolean" + } + }, "definitions": { - "build": { + "Host": { + "type": "string", + "enum": [ + "AppVeyor", + "AzurePipelines", + "Bamboo", + "Bitbucket", + "Bitrise", + "GitHubActions", + "GitLab", + "Jenkins", + "Rider", + "SpaceAutomation", + "TeamCity", + "Terminal", + "TravisCI", + "VisualStudio", + "VSCode" + ] + }, + "SignPathSettings": { "type": "object", "properties": { - "AutoStash": { - "type": "boolean" - }, - "CodecovToken": { - "type": "string", - "default": "Secrets must be entered via 'nuke :secrets [profile]'" + "OrganizationId": { + "type": [ + "null", + "string" + ] }, - "Configuration": { - "type": "string", - "enum": [ - "Debug", - "Release" + "ProjectSlug": { + "type": [ + "null", + "string" ] }, + "PolicySlug": { + "type": [ + "null", + "string" + ] + } + } + }, + "ExecutableTarget": { + "type": "string", + "enum": [ + "Announce", + "AnnounceDiscord", + "AnnounceMastodon", + "AnnounceSlack", + "AnnounceTwitter", + "Changelog", + "CheckoutExternalRepositories", + "Clean", + "Compile", + "CreateGitHubRelease", + "DeletePackages", + "DownloadLicenses", + "GenerateGlobalSolution", + "GeneratePublicApi", + "GenerateTools", + "Hotfix", + "Install", + "InstallFonts", + "Milestone", + "Pack", + "Publish", + "References", + "Release", + "ReleaseImage", + "ReportCoverage", + "ReportDuplicates", + "ReportIssues", + "Restore", + "RunTargetInDockerImageTest", + "SignPackages", + "Test", + "UpdateContributors", + "UpdateStargazers" + ] + }, + "Verbosity": { + "type": "string", + "description": "", + "enum": [ + "Verbose", + "Normal", + "Minimal", + "Quiet" + ] + }, + "NukeBuild": { + "properties": { "Continue": { "type": "boolean", "description": "Indicates to continue a previously failed build attempt" }, - "DiscordWebhook": { - "type": "string", - "default": "Secrets must be entered via 'nuke :secrets [profile]'" - }, - "FeedzNuGetApiKey": { - "type": "string", - "default": "Secrets must be entered via 'nuke :secrets [profile]'" - }, - "GitHubReleaseGitHubToken": { - "type": "string", - "default": "Secrets must be entered via 'nuke :secrets [profile]'" - }, "Help": { "type": "boolean", "description": "Shows the help text for this build assembly" }, "Host": { - "type": "string", "description": "Host for execution. Default is 'automatic'", - "enum": [ - "AppVeyor", - "AzurePipelines", - "Bamboo", - "Bitbucket", - "Bitrise", - "GitHubActions", - "GitLab", - "Jenkins", - "Rider", - "SpaceAutomation", - "TeamCity", - "Terminal", - "TravisCI", - "VisualStudio", - "VSCode" - ] - }, - "IgnoreFailedSources": { - "type": "boolean", - "description": "Ignore unreachable sources during Restore" - }, - "Major": { - "type": "boolean" - }, - "MastodonAccessToken": { - "type": "string", - "default": "Secrets must be entered via 'nuke :secrets [profile]'" + "$ref": "#/definitions/Host" }, "NoLogo": { "type": "boolean", @@ -91,152 +206,30 @@ "type": "string" } }, - "PublicNuGetApiKey": { - "type": "string", - "default": "Secrets must be entered via 'nuke :secrets [profile]'" - }, "Root": { "type": "string", "description": "Root directory during build execution" }, - "SignPathApiToken": { - "type": "string", - "default": "Secrets must be entered via 'nuke :secrets [profile]'" - }, - "SignPathOrganizationId": { - "type": "string" - }, - "SignPathPolicySlug": { - "type": "string" - }, - "SignPathProjectSlug": { - "type": "string" - }, "Skip": { "type": "array", "description": "List of targets to be skipped. Empty list skips all dependencies", "items": { - "type": "string", - "enum": [ - "Announce", - "AnnounceDiscord", - "AnnounceMastodon", - "AnnounceSlack", - "AnnounceTwitter", - "Changelog", - "CheckoutExternalRepositories", - "Clean", - "Compile", - "CreateGitHubRelease", - "DeletePackages", - "DownloadLicenses", - "GenerateGlobalSolution", - "GeneratePublicApi", - "GenerateTools", - "Hotfix", - "Install", - "InstallFonts", - "Milestone", - "Pack", - "Publish", - "References", - "Release", - "ReleaseImage", - "ReportCoverage", - "ReportDuplicates", - "ReportIssues", - "Restore", - "RunTargetInDockerImageTest", - "SignPackages", - "Test", - "UpdateContributors", - "UpdateStargazers" - ] + "$ref": "#/definitions/ExecutableTarget" } }, - "SlackWebhook": { - "type": "string", - "default": "Secrets must be entered via 'nuke :secrets [profile]'" - }, - "Solution": { - "type": "string", - "description": "Path to a solution file that is automatically loaded" - }, "Target": { "type": "array", "description": "List of targets to be invoked. Default is '{default_target}'", "items": { - "type": "string", - "enum": [ - "Announce", - "AnnounceDiscord", - "AnnounceMastodon", - "AnnounceSlack", - "AnnounceTwitter", - "Changelog", - "CheckoutExternalRepositories", - "Clean", - "Compile", - "CreateGitHubRelease", - "DeletePackages", - "DownloadLicenses", - "GenerateGlobalSolution", - "GeneratePublicApi", - "GenerateTools", - "Hotfix", - "Install", - "InstallFonts", - "Milestone", - "Pack", - "Publish", - "References", - "Release", - "ReleaseImage", - "ReportCoverage", - "ReportDuplicates", - "ReportIssues", - "Restore", - "RunTargetInDockerImageTest", - "SignPackages", - "Test", - "UpdateContributors", - "UpdateStargazers" - ] + "$ref": "#/definitions/ExecutableTarget" } }, - "TestDegreeOfParallelism": { - "type": "integer" - }, - "TwitterAccessToken": { - "type": "string", - "default": "Secrets must be entered via 'nuke :secrets [profile]'" - }, - "TwitterAccessTokenSecret": { - "type": "string", - "default": "Secrets must be entered via 'nuke :secrets [profile]'" - }, - "TwitterConsumerKey": { - "type": "string", - "default": "Secrets must be entered via 'nuke :secrets [profile]'" - }, - "TwitterConsumerSecret": { - "type": "string", - "default": "Secrets must be entered via 'nuke :secrets [profile]'" - }, - "UseHttps": { - "type": "boolean" - }, "Verbosity": { - "type": "string", "description": "Logging verbosity during build execution. Default is 'Normal'", - "enum": [ - "Minimal", - "Normal", - "Quiet", - "Verbose" - ] + "$ref": "#/definitions/Verbosity" } } } - } + }, + "$ref": "#/definitions/NukeBuild" } diff --git a/.nuke/parameters.json b/.nuke/parameters.json index 458f25181..589f2e3fb 100644 --- a/.nuke/parameters.json +++ b/.nuke/parameters.json @@ -1,7 +1,9 @@ { - "$schema": "./build.schema.json", + "$schema": "build.schema.json", "Solution": "nuke-common.sln", - "SignPathOrganizationId": "0fdaf334-6910-41f4-83d2-e58e4cccb087", - "SignPathProjectSlug": "nuke", - "SignPathPolicySlug": "release-signing" + "SignPathSettings": { + "OrganizationId": "0fdaf334-6910-41f4-83d2-e58e4cccb087", + "ProjectSlug": "nuke", + "PolicySlug": "release-signing" + } } diff --git a/.teamcity/settings.kts b/.teamcity/settings.kts index 2288fd20a..62b866404 100644 --- a/.teamcity/settings.kts +++ b/.teamcity/settings.kts @@ -63,21 +63,9 @@ project { unchecked = "False", display = ParameterDisplay.NORMAL) text ( - "env.SignPathOrganizationId", - label = "SignPathOrganizationId", - value = "0fdaf334-6910-41f4-83d2-e58e4cccb087", - allowEmpty = true, - display = ParameterDisplay.NORMAL) - text ( - "env.SignPathPolicySlug", - label = "SignPathPolicySlug", - value = "release-signing", - allowEmpty = true, - display = ParameterDisplay.NORMAL) - text ( - "env.SignPathProjectSlug", - label = "SignPathProjectSlug", - value = "nuke", + "env.SignPathSettings", + label = "SignPathSettings", + value = "", allowEmpty = true, display = ParameterDisplay.NORMAL) text ( diff --git a/Directory.Packages.props b/Directory.Packages.props new file mode 100644 index 000000000..00c9ff6d0 --- /dev/null +++ b/Directory.Packages.props @@ -0,0 +1,84 @@ + + + + true + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/build/_build.csproj b/build/_build.csproj index b62c736dc..8de4e02f5 100644 --- a/build/_build.csproj +++ b/build/_build.csproj @@ -10,6 +10,7 @@ CS0649;CS0169 .\.. 1 + false diff --git a/docs/03-common/06-versioning.md b/docs/03-common/06-versioning.md index 2a581b63d..f726d0079 100644 --- a/docs/03-common/06-versioning.md +++ b/docs/03-common/06-versioning.md @@ -86,7 +86,7 @@ Please refer to the [MinVer documentation](https://github.com/adamralph/minver#u ```powershell title="Tool Installation" # terminal-command -nuke :add-package MinVer +nuke :add-package minver-cli ``` ```csharp title="Build.cs" diff --git a/nuke-common.sln.DotSettings b/nuke-common.sln.DotSettings index 08232f1f4..ece833ace 100644 --- a/nuke-common.sln.DotSettings +++ b/nuke-common.sln.DotSettings @@ -1,5 +1,5 @@  - Copyright $CURRENT_YEAR$ Maintainers of NUKE. + Copyright ${CurrentDate.Year} Maintainers of NUKE. Distributed under the MIT License. https://github.com/nuke-build/nuke/blob/master/LICENSE <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb_AaBb" /> @@ -17,4 +17,5 @@ https://github.com/nuke-build/nuke/blob/master/LICENSE 3 True 1 + True diff --git a/source/Directory.Build.props b/source/Directory.Build.props index c67af9847..3461e7ae7 100644 --- a/source/Directory.Build.props +++ b/source/Directory.Build.props @@ -36,19 +36,19 @@ - + - - - - - - - - - + + + + + + + + + diff --git a/source/Nuke.Build.Shared/CompletionUtility.cs b/source/Nuke.Build.Shared/CompletionUtility.cs index 327eb816b..edaaaca26 100644 --- a/source/Nuke.Build.Shared/CompletionUtility.cs +++ b/source/Nuke.Build.Shared/CompletionUtility.cs @@ -22,17 +22,47 @@ public static IReadOnlyDictionary GetItemsFromSchema(AbsoluteP public static IReadOnlyDictionary GetItemsFromSchema(JsonDocument schema, IEnumerable profileNames) { - string[] GetEnumValues(JsonElement property) - => property.TryGetProperty("enum", out var enumProperty) - ? enumProperty.EnumerateArray().Select(x => x.GetString()).ToArray() - : property.TryGetProperty("items", out var itemsProperty) - ? GetEnumValues(itemsProperty) - : null; - - var properties = schema.RootElement.GetProperty("definitions").GetProperty("build").GetProperty("properties") - .EnumerateObject().OfType(); - return properties.ToDictionary(x => x.Name, x => GetEnumValues(x.Value)) + var definitions = schema.RootElement.GetProperty("definitions").EnumerateObject().ToDictionary(x => x.Name, x => x); + + var parameterProperties = schema.RootElement.GetProperty("definitions").TryGetProperty("NukeBuild", out var nukebuildProperty) + ? nukebuildProperty.GetProperty("properties").EnumerateObject() + .Concat(schema.RootElement.TryGetProperty("properties", out var properties) ? properties.EnumerateObject() : []) + : definitions["build"].Value.GetProperty("properties").EnumerateObject(); + + return parameterProperties + .Select(x => (x.Name, Values: GetValues(x))) + .Where(x => x.Values != null) + .ToDictionary(x => x.Name, x => x.Values) .SetKeyValue(Constants.LoadedLocalProfilesParameterName, profileNames.ToArray()).AsReadOnly(); + + string[] GetValues(JsonProperty property) + { + if (property.Value.TryGetProperty("type", out var typeProperty)) + { + var types = typeProperty.ValueKind != JsonValueKind.Array + ? [typeProperty.GetString()] + : typeProperty.EnumerateArray().Select(x => x.GetString()).ToArray(); + if (types.ContainsAnyOrdinalIgnoreCase(["string", "boolean", "integer"])) + { + return property.Value.TryGetProperty("enum", out var enumProperty) + ? enumProperty.EnumerateArray().Select(x => x.GetString()).ToArray() + : []; + } + + if (types.Single() == "array") + return GetValues(property.Value.EnumerateObject().Single(x => x.Name == "items")); + + if (types.Single() == "object") + return null; + } + else if (property.Value.TryGetProperty("$ref", out var refProperty)) + { + var definition = definitions.GetValueOrDefault(refProperty.GetString().NotNull().Split('/').LastOrDefault()); + return GetValues(definition); + } + + throw new NotSupportedException(); + } } // ReSharper disable once CognitiveComplexity diff --git a/source/Nuke.Build.Shared/Nuke.Build.Shared.csproj b/source/Nuke.Build.Shared/Nuke.Build.Shared.csproj index 360fe16cb..5925ae70e 100644 --- a/source/Nuke.Build.Shared/Nuke.Build.Shared.csproj +++ b/source/Nuke.Build.Shared/Nuke.Build.Shared.csproj @@ -9,7 +9,7 @@ - + diff --git a/source/Nuke.Build.Tests/CompletionUtilityTest.TestGetCompletionItemsFromSchema.verified.txt b/source/Nuke.Build.Tests/CompletionUtilityTest.TestGetCompletionItemsFromSchema.verified.txt new file mode 100644 index 000000000..9387e8cae --- /dev/null +++ b/source/Nuke.Build.Tests/CompletionUtilityTest.TestGetCompletionItemsFromSchema.verified.txt @@ -0,0 +1,14 @@ +{ + Configuration: [ + Debug, + Release + ], + NoLogo: [], + Profile: [ + dev + ], + Target: [ + Restore, + Compile + ] +} \ No newline at end of file diff --git a/source/Nuke.Build.Tests/CompletionUtilityTest.TestGetCompletionItemsParameterBuild.verified.txt b/source/Nuke.Build.Tests/CompletionUtilityTest.TestGetCompletionItemsParameterBuild.verified.txt new file mode 100644 index 000000000..cfdb64000 --- /dev/null +++ b/source/Nuke.Build.Tests/CompletionUtilityTest.TestGetCompletionItemsParameterBuild.verified.txt @@ -0,0 +1,38 @@ +{ + BooleanParam: [], + ComponentInheritedParam: [], + Continue: [], + CustomEnumerationArrayParam: [ + Debug, + Release + ], + CustomEnumerationParam: [ + Debug, + Release + ], + Help: [], + Host: [ + Rider, + Terminal, + VisualStudio, + VSCode + ], + IntegerArrayParam: [], + NoLogo: [], + NullableBooleanParam: [], + Partition: [], + Plan: [], + Profile: [], + RegularParam: [], + Root: [], + SecretParam: [], + Skip: [], + StringArrayParam: [], + Target: [], + Verbosity: [ + Verbose, + Normal, + Minimal, + Quiet + ] +} \ No newline at end of file diff --git a/source/Nuke.Build.Tests/CompletionUtilityTest.TestGetCompletionItemsTargetBuild.verified.txt b/source/Nuke.Build.Tests/CompletionUtilityTest.TestGetCompletionItemsTargetBuild.verified.txt new file mode 100644 index 000000000..84f42e6f5 --- /dev/null +++ b/source/Nuke.Build.Tests/CompletionUtilityTest.TestGetCompletionItemsTargetBuild.verified.txt @@ -0,0 +1,35 @@ +{ + Continue: [], + Help: [], + Host: [ + Rider, + Terminal, + VisualStudio, + VSCode + ], + NoLogo: [], + Partition: [], + Plan: [], + Profile: [ + dev + ], + Root: [], + Skip: [ + ExplicitTarget, + ImplementedTarget, + InheritedTarget, + RegularTarget + ], + Target: [ + ExplicitTarget, + ImplementedTarget, + InheritedTarget, + RegularTarget + ], + Verbosity: [ + Verbose, + Normal, + Minimal, + Quiet + ] +} \ No newline at end of file diff --git a/source/Nuke.Build.Tests/CompletionUtilityTest.cs b/source/Nuke.Build.Tests/CompletionUtilityTest.cs index 440aaaa42..35ccab8e4 100644 --- a/source/Nuke.Build.Tests/CompletionUtilityTest.cs +++ b/source/Nuke.Build.Tests/CompletionUtilityTest.cs @@ -6,16 +6,48 @@ using System.Collections.Generic; using System.Linq; using System.Text.Json; +using System.Threading.Tasks; using FluentAssertions; +using Nuke.Common.IO; using Nuke.Common.Utilities; +using VerifyTests; +using VerifyXunit; using Xunit; namespace Nuke.Common.Tests; public class CompletionUtilityTest { + private readonly VerifySettings _verifySettings; + private static AbsolutePath RootDirectory => Constants.TryGetRootDirectoryFrom(EnvironmentInfo.WorkingDirectory).NotNull(); + private static AbsolutePath TestDirectory => RootDirectory / "source" / "Nuke.Build.Tests"; + + public CompletionUtilityTest() + { + _verifySettings = new VerifySettings(); + _verifySettings.DontIgnoreEmptyCollections(); + } + + [Fact] + public async Task TestGetCompletionItemsTargetBuild() + { + var file = TestDirectory / "SchemaUtilityTest.TestTargetBuild.verified.json"; + var schema = JsonDocument.Parse(file.ReadAllText()); + var items = CompletionUtility.GetItemsFromSchema(schema, new[] { "dev" }); + await Verifier.Verify(items, _verifySettings); + } + [Fact] - public void TestGetCompletionItemsFromSchema() + public async Task TestGetCompletionItemsParameterBuild() + { + var file = TestDirectory / "SchemaUtilityTest.TestParameterBuild.verified.json"; + var schema = JsonDocument.Parse(file.ReadAllText()); + var items = CompletionUtility.GetItemsFromSchema(schema, []); + await Verifier.Verify(items, _verifySettings); + } + + [Fact] + public async Task TestGetCompletionItemsFromSchema() { var schema = JsonDocument.Parse(""" { @@ -62,14 +94,7 @@ public void TestGetCompletionItemsFromSchema() """); var profileNames = new[] { "dev" }; var items = CompletionUtility.GetItemsFromSchema(schema, profileNames); - items.Should().BeEquivalentTo( - new Dictionary - { - ["NoLogo"] = null, - ["Configuration"] = new[] { "Debug", "Release" }, - ["Target"] = new[] { "Restore", "Compile" }, - [Constants.LoadedLocalProfilesParameterName] = profileNames - }); + await Verifier.Verify(items, _verifySettings); } [Theory] @@ -94,9 +119,9 @@ public void TestGetRelevantCompletionItems(string words, string[] expectedItems) var completionItems = new Dictionary { - { Constants.InvokedTargetsParameterName, new[] { "Compile", "GitHubPublish" } }, - { "ApiKey", null }, - { "NuGetSource", null } + { Constants.InvokedTargetsParameterName, ["Compile", "GitHubPublish"] }, + { "ApiKey", [] }, + { "NuGetSource", [] } }; CompletionUtility.GetRelevantItems(words, completionItems) .Should() diff --git a/source/Nuke.Build.Tests/ParameterServiceTest.cs b/source/Nuke.Build.Tests/ParameterServiceTest.cs index 59f94c307..5ca5c5468 100644 --- a/source/Nuke.Build.Tests/ParameterServiceTest.cs +++ b/source/Nuke.Build.Tests/ParameterServiceTest.cs @@ -48,12 +48,11 @@ public void TestNotSupplied(Type destinationType, object expectedValue) public void TestEnvironmentVariables(string parameter, Type destinationType, object expectedValue) { var service = GetService( - new[] - { + [ "-arg1", "value1", "-switch1" - }, + ], new Dictionary { { "arg1", "value2" }, @@ -68,15 +67,14 @@ public void TestEnvironmentVariables(string parameter, Type destinationType, obj public void TestExpression() { var service = GetService( - new[] - { + [ "--string", "--set", "1", "2", "3", "--interface-param" - }); + ]); var build = new TestBuild(); @@ -98,12 +96,12 @@ public void TestValueSet() { var build = new TestBuild(); var verbosities = new[] - { - (nameof(Verbosity.Minimal), Verbosity.Minimal), - (nameof(Verbosity.Normal), Verbosity.Normal), - (nameof(Verbosity.Quiet), Verbosity.Quiet), - (nameof(Verbosity.Verbose), Verbosity.Verbose), - }; + { + (nameof(Verbosity.Minimal), Verbosity.Minimal), + (nameof(Verbosity.Normal), Verbosity.Normal), + (nameof(Verbosity.Quiet), Verbosity.Quiet), + (nameof(Verbosity.Verbose), Verbosity.Verbose), + }; ParameterService.GetParameterValueSet(GetMemberInfo(() => NukeBuild.Verbosity), instance: null) .Should().BeEquivalentTo(verbosities); ParameterService.GetParameterValueSet(GetMemberInfo(() => build.Verbosities), instance: null) @@ -118,13 +116,13 @@ public void TestPrecedence() var environmentVariables = new Dictionary { ["string"] = "environmentVariables" }; var commandLineArguments = new ArgumentParser("--string commandLine"); - var parameterFileArguments = new ArgumentParser("--string parameterFile"); + var parameterFileArguments = new Func((_, _) => "parameterFile"); var commitMessageArguments = new ArgumentParser("--string commitMessage"); var service = new ParameterService(() => emptyArguments, () => emptyEnvironmentVariables) - { - ArgumentsFromFilesService = parameterFileArguments - }; + { + ArgumentsFromFilesService = parameterFileArguments + }; service.GetParameter("string", typeof(string), separator: null).Should().Be("parameterFile"); service = new ParameterService(() => emptyArguments, () => environmentVariables) { ArgumentsFromFilesService = parameterFileArguments }; @@ -134,10 +132,10 @@ public void TestPrecedence() service.GetParameter("string", typeof(string), separator: null).Should().Be("commandLine"); service = new ParameterService(() => emptyArguments, () => emptyEnvironmentVariables) - { - ArgumentsFromFilesService = parameterFileArguments, - ArgumentsFromCommitMessageService = commitMessageArguments - }; + { + ArgumentsFromFilesService = parameterFileArguments, + ArgumentsFromCommitMessageService = commitMessageArguments + }; service.GetParameter("string", typeof(string), separator: null).Should().Be("commitMessage"); } diff --git a/source/Nuke.Build.Tests/SchemaUtilityTest.TestCustomParameterAttributeAttribute.verified.json b/source/Nuke.Build.Tests/SchemaUtilityTest.TestCustomParameterAttributeAttribute.verified.json new file mode 100644 index 000000000..61883c8cf --- /dev/null +++ b/source/Nuke.Build.Tests/SchemaUtilityTest.TestCustomParameterAttributeAttribute.verified.json @@ -0,0 +1,90 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "properties": { + "ComplexTypeParamWithAttribute": { + "type": "string" + } + }, + "definitions": { + "Host": { + "type": "string", + "enum": [ + "Rider", + "Terminal", + "VisualStudio", + "VSCode" + ] + }, + "ExecutableTarget": { + "type": "string" + }, + "Verbosity": { + "type": "string", + "description": "", + "enum": [ + "Verbose", + "Normal", + "Minimal", + "Quiet" + ] + }, + "NukeBuild": { + "properties": { + "Continue": { + "type": "boolean", + "description": "Indicates to continue a previously failed build attempt" + }, + "Help": { + "type": "boolean", + "description": "Shows the help text for this build assembly" + }, + "Host": { + "description": "Host for execution. Default is 'automatic'", + "$ref": "#/definitions/Host" + }, + "NoLogo": { + "type": "boolean", + "description": "Disables displaying the NUKE logo" + }, + "Partition": { + "type": "string", + "description": "Partition to use on CI" + }, + "Plan": { + "type": "boolean", + "description": "Shows the execution plan (HTML)" + }, + "Profile": { + "type": "array", + "description": "Defines the profiles to load", + "items": { + "type": "string" + } + }, + "Root": { + "type": "string", + "description": "Root directory during build execution" + }, + "Skip": { + "type": "array", + "description": "List of targets to be skipped. Empty list skips all dependencies", + "items": { + "$ref": "#/definitions/ExecutableTarget" + } + }, + "Target": { + "type": "array", + "description": "List of targets to be invoked. Default is '{default_target}'", + "items": { + "$ref": "#/definitions/ExecutableTarget" + } + }, + "Verbosity": { + "description": "Logging verbosity during build execution. Default is 'Normal'", + "$ref": "#/definitions/Verbosity" + } + } + } + }, + "$ref": "#/definitions/NukeBuild" +} \ No newline at end of file diff --git a/source/Nuke.Build.Tests/SchemaUtilityTest.TestEmptyBuild.verified.json b/source/Nuke.Build.Tests/SchemaUtilityTest.TestEmptyBuild.verified.json new file mode 100644 index 000000000..997001969 --- /dev/null +++ b/source/Nuke.Build.Tests/SchemaUtilityTest.TestEmptyBuild.verified.json @@ -0,0 +1,85 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "definitions": { + "Host": { + "type": "string", + "enum": [ + "Rider", + "Terminal", + "VisualStudio", + "VSCode" + ] + }, + "ExecutableTarget": { + "type": "string" + }, + "Verbosity": { + "type": "string", + "description": "", + "enum": [ + "Verbose", + "Normal", + "Minimal", + "Quiet" + ] + }, + "NukeBuild": { + "properties": { + "Continue": { + "type": "boolean", + "description": "Indicates to continue a previously failed build attempt" + }, + "Help": { + "type": "boolean", + "description": "Shows the help text for this build assembly" + }, + "Host": { + "description": "Host for execution. Default is 'automatic'", + "$ref": "#/definitions/Host" + }, + "NoLogo": { + "type": "boolean", + "description": "Disables displaying the NUKE logo" + }, + "Partition": { + "type": "string", + "description": "Partition to use on CI" + }, + "Plan": { + "type": "boolean", + "description": "Shows the execution plan (HTML)" + }, + "Profile": { + "type": "array", + "description": "Defines the profiles to load", + "items": { + "type": "string" + } + }, + "Root": { + "type": "string", + "description": "Root directory during build execution" + }, + "Skip": { + "type": "array", + "description": "List of targets to be skipped. Empty list skips all dependencies", + "items": { + "$ref": "#/definitions/ExecutableTarget" + } + }, + "Target": { + "type": "array", + "description": "List of targets to be invoked. Default is '{default_target}'", + "items": { + "$ref": "#/definitions/ExecutableTarget" + } + }, + "Verbosity": { + "description": "Logging verbosity during build execution. Default is 'Normal'", + "$ref": "#/definitions/Verbosity" + } + } + } + }, + "$ref": "#/definitions/NukeBuild" +} diff --git a/source/Nuke.Build.Tests/SchemaUtilityTest.TestGetBuildSchema.verified.txt b/source/Nuke.Build.Tests/SchemaUtilityTest.TestGetBuildSchema.verified.json similarity index 71% rename from source/Nuke.Build.Tests/SchemaUtilityTest.TestGetBuildSchema.verified.txt rename to source/Nuke.Build.Tests/SchemaUtilityTest.TestGetBuildSchema.verified.json index 1655c4007..31e7c1171 100644 --- a/source/Nuke.Build.Tests/SchemaUtilityTest.TestGetBuildSchema.verified.txt +++ b/source/Nuke.Build.Tests/SchemaUtilityTest.TestGetBuildSchema.verified.json @@ -6,40 +6,63 @@ "build": { "type": "object", "properties": { - "ComponentParam1": { + "BooleanParam": { + "type": "boolean" + }, + "ComplexTypeArrayParam": { + "type": "array", + "items": { + "type": "string" + } + }, + "ComplexTypeParam": { + "type": "string" + }, + "ComponentInheritedParam": { "type": "string" }, "Continue": { "type": "boolean", "description": "Indicates to continue a previously failed build attempt" }, + "CustomEnumerationArrayParam": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "Debug", + "Release" + ] + } + }, + "CustomEnumerationParam": { + "type": "string", + "enum": [ + "Debug", + "Release" + ] + }, "Help": { "type": "boolean", "description": "Shows the help text for this build assembly" }, "Host": { "type": "string", - "description": "Host for execution. Default is 'automatic'", - "enum": [ - "Rider", - "Terminal", - "VisualStudio", - "VSCode" - ] + "description": "Host for execution. Default is 'automatic'" + }, + "IntegerArrayParam": { + "type": "array", + "items": { + "type": "integer" + } }, "NoLogo": { "type": "boolean", "description": "Disables displaying the NUKE logo" }, - "NullableBool": { + "NullableBooleanParam": { "type": "boolean" }, - "NullableInteger": { - "type": "integer" - }, - "Param": { - "type": "string" - }, "Partition": { "type": "string", "description": "Partition to use on CI" @@ -55,11 +78,14 @@ "type": "string" } }, + "RegularParam": { + "type": "string" + }, "Root": { "type": "string", "description": "Root directory during build execution" }, - "Secret": { + "SecretParam": { "type": "string", "default": "Secrets must be entered via 'nuke :secrets [profile]'" }, @@ -67,36 +93,20 @@ "type": "array", "description": "List of targets to be skipped. Empty list skips all dependencies", "items": { - "type": "string", - "enum": [ - "Bar", - "Foo", - "Zoo" - ] + "type": "string" } }, - "Target": { + "StringArrayParam": { "type": "array", - "description": "List of targets to be invoked. Default is '{default_target}'", "items": { - "type": "string", - "enum": [ - "Bar", - "Foo", - "Zoo" - ] + "type": "string" } }, - "Verbosities": { + "Target": { "type": "array", + "description": "List of targets to be invoked. Default is '{default_target}'", "items": { - "type": "string", - "enum": [ - "Minimal", - "Normal", - "Quiet", - "Verbose" - ] + "type": "string" } }, "Verbosity": { diff --git a/source/Nuke.Build.Tests/SchemaUtilityTest.TestParameterBuild.verified.json b/source/Nuke.Build.Tests/SchemaUtilityTest.TestParameterBuild.verified.json new file mode 100644 index 000000000..124ede2f2 --- /dev/null +++ b/source/Nuke.Build.Tests/SchemaUtilityTest.TestParameterBuild.verified.json @@ -0,0 +1,190 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "properties": { + "BooleanParam": { + "type": "boolean" + }, + "ComplexTypeArrayParam": { + "type": "array", + "items": { + "$ref": "#/definitions/ComplexType" + } + }, + "ComplexTypeParam": { + "$ref": "#/definitions/ComplexType" + }, + "ComponentInheritedParam": { + "type": "string" + }, + "CustomEnumerationArrayParam": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "Debug", + "Release" + ] + } + }, + "CustomEnumerationParam": { + "type": "string", + "enum": [ + "Debug", + "Release" + ] + }, + "IntegerArrayParam": { + "type": "array", + "items": { + "type": "integer", + "format": "int32" + } + }, + "NullableBooleanParam": { + "type": [ + "boolean", + "null" + ] + }, + "RegularParam": { + "type": "string" + }, + "SecretParam": { + "type": "string", + "default": "Secrets must be entered via 'nuke :secrets [profile]'" + }, + "StringArrayParam": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "definitions": { + "ComplexType": { + "type": "object", + "properties": { + "String": { + "type": [ + "null", + "string" + ] + }, + "Number": { + "type": "integer", + "format": "int32" + }, + "Paths": { + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + } + }, + "SubObject": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/definitions/ComplexSubType" + } + ] + } + } + }, + "ComplexSubType": { + "type": "object", + "properties": { + "Boolean": { + "type": [ + "boolean", + "null" + ] + } + } + }, + "Host": { + "type": "string", + "enum": [ + "Rider", + "Terminal", + "VisualStudio", + "VSCode" + ] + }, + "ExecutableTarget": { + "type": "string" + }, + "Verbosity": { + "type": "string", + "description": "", + "enum": [ + "Verbose", + "Normal", + "Minimal", + "Quiet" + ] + }, + "NukeBuild": { + "properties": { + "Continue": { + "type": "boolean", + "description": "Indicates to continue a previously failed build attempt" + }, + "Help": { + "type": "boolean", + "description": "Shows the help text for this build assembly" + }, + "Host": { + "description": "Host for execution. Default is 'automatic'", + "$ref": "#/definitions/Host" + }, + "NoLogo": { + "type": "boolean", + "description": "Disables displaying the NUKE logo" + }, + "Partition": { + "type": "string", + "description": "Partition to use on CI" + }, + "Plan": { + "type": "boolean", + "description": "Shows the execution plan (HTML)" + }, + "Profile": { + "type": "array", + "description": "Defines the profiles to load", + "items": { + "type": "string" + } + }, + "Root": { + "type": "string", + "description": "Root directory during build execution" + }, + "Skip": { + "type": "array", + "description": "List of targets to be skipped. Empty list skips all dependencies", + "items": { + "$ref": "#/definitions/ExecutableTarget" + } + }, + "Target": { + "type": "array", + "description": "List of targets to be invoked. Default is '{default_target}'", + "items": { + "$ref": "#/definitions/ExecutableTarget" + } + }, + "Verbosity": { + "description": "Logging verbosity during build execution. Default is 'Normal'", + "$ref": "#/definitions/Verbosity" + } + } + } + }, + "$ref": "#/definitions/NukeBuild" +} \ No newline at end of file diff --git a/source/Nuke.Build.Tests/SchemaUtilityTest.TestTargetBuild.verified.json b/source/Nuke.Build.Tests/SchemaUtilityTest.TestTargetBuild.verified.json new file mode 100644 index 000000000..25a38901b --- /dev/null +++ b/source/Nuke.Build.Tests/SchemaUtilityTest.TestTargetBuild.verified.json @@ -0,0 +1,91 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "definitions": { + "Host": { + "type": "string", + "enum": [ + "Rider", + "Terminal", + "VisualStudio", + "VSCode" + ] + }, + "ExecutableTarget": { + "type": "string", + "enum": [ + "ExplicitTarget", + "ImplementedTarget", + "InheritedTarget", + "RegularTarget" + ] + }, + "Verbosity": { + "type": "string", + "description": "", + "enum": [ + "Verbose", + "Normal", + "Minimal", + "Quiet" + ] + }, + "NukeBuild": { + "properties": { + "Continue": { + "type": "boolean", + "description": "Indicates to continue a previously failed build attempt" + }, + "Help": { + "type": "boolean", + "description": "Shows the help text for this build assembly" + }, + "Host": { + "description": "Host for execution. Default is 'automatic'", + "$ref": "#/definitions/Host" + }, + "NoLogo": { + "type": "boolean", + "description": "Disables displaying the NUKE logo" + }, + "Partition": { + "type": "string", + "description": "Partition to use on CI" + }, + "Plan": { + "type": "boolean", + "description": "Shows the execution plan (HTML)" + }, + "Profile": { + "type": "array", + "description": "Defines the profiles to load", + "items": { + "type": "string" + } + }, + "Root": { + "type": "string", + "description": "Root directory during build execution" + }, + "Skip": { + "type": "array", + "description": "List of targets to be skipped. Empty list skips all dependencies", + "items": { + "$ref": "#/definitions/ExecutableTarget" + } + }, + "Target": { + "type": "array", + "description": "List of targets to be invoked. Default is '{default_target}'", + "items": { + "$ref": "#/definitions/ExecutableTarget" + } + }, + "Verbosity": { + "description": "Logging verbosity during build execution. Default is 'Normal'", + "$ref": "#/definitions/Verbosity" + } + } + } + }, + "$ref": "#/definitions/NukeBuild" +} diff --git a/source/Nuke.Build.Tests/SchemaUtilityTest.cs b/source/Nuke.Build.Tests/SchemaUtilityTest.cs index cb29d2472..559c02e00 100644 --- a/source/Nuke.Build.Tests/SchemaUtilityTest.cs +++ b/source/Nuke.Build.Tests/SchemaUtilityTest.cs @@ -3,52 +3,128 @@ // https://github.com/nuke-build/nuke/blob/master/LICENSE using System; +using System.ComponentModel; using System.Linq; -using System.Text.Encodings.Web; -using System.Text.Json; using System.Threading.Tasks; using Nuke.Common.Execution; +using Nuke.Common.IO; +using Nuke.Common.Tooling; +using Nuke.Common.Utilities; using VerifyXunit; using Xunit; +#pragma warning disable CS0169 // Field is never used + namespace Nuke.Common.Tests; public class SchemaUtilityTest { [Fact] - public Task TestGetBuildSchema() + public Task TestEmptyBuild() + { + var jsonSchema = SchemaUtility.GetJsonString(new EmptyBuild()); + return Verifier.Verify(jsonSchema, "json"); + } + + [Fact] + public Task TestTargetBuild() + { + var jsonSchema = SchemaUtility.GetJsonString(new TargetBuild()); + return Verifier.Verify(jsonSchema, "json"); + } + + [Fact] + public Task TestParameterBuild() + { + var jsonSchema = SchemaUtility.GetJsonString(new ParameterBuild()); + return Verifier.Verify(jsonSchema, "json"); + } + + [Fact] + public Task TestCustomParameterAttributeAttribute() + { + var jsonSchema = SchemaUtility.GetJsonString(new CustomParameterAttributeBuild()); + return Verifier.Verify(jsonSchema, "json"); + } + + // ReSharper disable All +#pragma warning disable CS0414 // Field is assigned but its value is never used +#pragma warning disable CS0649 // Field is never assigned to, and will always have its default value + private class EmptyBuild : NukeBuild { - var schema = SchemaUtility.GetBuildSchema(new TestBuild()); - var options = new JsonSerializerOptions { WriteIndented = true, Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping }; - var json = JsonSerializer.Serialize(schema, options); - return Verifier.Verify(json); } -#pragma warning disable CS0649 - private class TestBuild : NukeBuild, ITestComponent + private class TargetBuild : NukeBuild, ITargetComponent { - [Parameter] public string Param; - [Parameter] public bool? NullableBool; - [Parameter] public int? NullableInteger; - [Parameter] public Verbosity[] Verbosities; - [Parameter] [Secret] public string Secret; - public string Param2 => ""; - string ITestComponent.Param3 => ""; + Target RegularTarget => _ => _; + public Target ImplementedTarget => _ => _; + Target ITargetComponent.ExplicitTarget => _ => _; + } - Target ITestComponent.Bar => _ => _; - public Target Zoo => _ => _; + private interface ITargetComponent : INukeBuild + { + Target InheritedTarget => _ => _; + Target ImplementedTarget => _ => _; + Target ExplicitTarget => _ => _; + } + + private class ParameterBuild : NukeBuild, IParameterComponent + { + [Parameter] readonly string RegularParam; + [Parameter] [Secret] readonly string SecretParam; + + [Parameter] readonly bool BooleanParam; + [Parameter] readonly bool? NullableBooleanParam; + + [Parameter] readonly string[] StringArrayParam; + [Parameter] readonly int[] IntegerArrayParam; + + [Parameter] readonly CustomEnumeration CustomEnumerationParam; + [Parameter] readonly CustomEnumeration[] CustomEnumerationArrayParam; + + [Parameter] readonly ComplexType ComplexTypeParam; + [Parameter] readonly ComplexType[] ComplexTypeArrayParam; } [ParameterPrefix("Component")] - private interface ITestComponent : INukeBuild + private interface IParameterComponent : INukeBuild { - [Parameter] string Param1 => TryGetValue(() => Param1); - [Parameter] string Param2 => TryGetValue(() => Param2); - [Parameter] string Param3 => TryGetValue(() => Param3); + [Parameter] string InheritedParam => TryGetValue(() => InheritedParam); + } - Target Foo => _ => _; - Target Bar => _ => _; - Target Zoo => _ => _; + private class ComplexType + { + public string String; + public int Number; + public AbsolutePath[] Paths; + public ComplexSubType SubObject; + } + + private class ComplexSubType + { + public bool? Boolean; } -#pragma warning restore CS0649 + + [TypeConverter(typeof(TypeConverter))] + private class CustomEnumeration : Enumeration + { + public static CustomEnumeration Debug = new() { Value = nameof(Debug) }; + public static CustomEnumeration Release = new() { Value = nameof(Release) }; + + public static implicit operator string(CustomEnumeration configuration) + { + return configuration.Value; + } + } + + private class CustomParameterAttributeBuild : NukeBuild + { + [CustomParameter] readonly ComplexType ComplexTypeParamWithAttribute; + } + + private class CustomParameterAttribute : ParameterAttribute; + +#pragma warning restore CS0649 // Field is never assigned to, and will always have its default value +#pragma warning restore CS0414 // Field is assigned but its value is never used + // ReSharper restore All } diff --git a/source/Nuke.Build.Tests/parameters.json b/source/Nuke.Build.Tests/parameters.json new file mode 100644 index 000000000..0b2ee0afb --- /dev/null +++ b/source/Nuke.Build.Tests/parameters.json @@ -0,0 +1,5 @@ +{ + "$schema": "./SchemaUtilityTest.TestTargetBuild.verified.json", + "Verbosity": "Minimal", + "Target": ["ExplicitTarget", "InheritedTarget"] +} diff --git a/source/Nuke.Build/Execution/Extensions/ArgumentsFromParametersFileAttribute.cs b/source/Nuke.Build/Execution/Extensions/ArgumentsFromParametersFileAttribute.cs index 1a2b1c97d..2baaff2dc 100644 --- a/source/Nuke.Build/Execution/Extensions/ArgumentsFromParametersFileAttribute.cs +++ b/source/Nuke.Build/Execution/Extensions/ArgumentsFromParametersFileAttribute.cs @@ -13,33 +13,51 @@ using Nuke.Common.Utilities; using Nuke.Common.Utilities.Collections; using Nuke.Common.ValueInjection; -using Serilog; namespace Nuke.Common.Execution; -internal class ArgumentsFromParametersFileAttribute : BuildExtensionAttributeBase, IOnBuildCreated +[PublicAPI] +public class ArgumentsFromParametersFileAttribute : BuildExtensionAttributeBase, IOnBuildCreated { public void OnBuildCreated(IReadOnlyCollection executableTargets) { // TODO: probably remove - if (!Directory.Exists(Constants.GetNukeDirectory(Build.RootDirectory))) + if (!Constants.GetNukeDirectory(NukeBuild.RootDirectory).DirectoryExists()) return; + + // IEnumerable ConvertToArguments(string profile, string name, string[] values) + // { + // var member = parameterMembers.SingleOrDefault(x => ParameterService.GetParameterMemberName(x).EqualsOrdinalIgnoreCase(name)); + // var scalarType = member?.GetMemberType().GetScalarType(); + // var mustDecrypt = (member?.HasCustomAttribute() ?? false) && !BuildServerConfigurationGeneration.IsActive; + // var decryptedValues = values.Select(x => mustDecrypt ? DecryptValue(profile, name, x) : x); + // var convertedValues = decryptedValues.Select(x => ConvertValue(scalarType, x)).ToList(); + // Log.Verbose("Passing value for {Member} ({Value})", + // member?.GetDisplayName() ?? "", + // !mustDecrypt ? convertedValues.JoinComma() : "secret"); + // return new[] { $"--{ParameterService.GetParameterDashedName(name)}" }.Concat(convertedValues); + // } + // + + // + // // TODO: Abstract AbsolutePath/Solution/Project etc. + // string ConvertValue(Type scalarType, string value) + // => scalarType == typeof(AbsolutePath) || + // typeof(Solution).IsAssignableFrom(scalarType) || + // scalarType == typeof(Project) + // ? EnvironmentInfo.WorkingDirectory.GetUnixRelativePathTo(NukeBuild.RootDirectory / value) + // : value; + var parameterMembers = ValueInjectionUtility.GetParameterMembers(Build.GetType(), includeUnlisted: true); - var passwords = new Dictionary(); + var jobjectsAndProfiles = new[] { (File: Constants.GetDefaultParametersFile(NukeBuild.RootDirectory), Profile: Constants.DefaultProfileName) } + .Where(x => File.Exists(x.File)) + .Concat(NukeBuild.LoadedLocalProfiles.Select(x => (File: Constants.GetParametersProfileFile(NukeBuild.RootDirectory, x), Profile: x))) + .ForEachLazy(x => Assert.FileExists(x.File)) + .Select(x => (JObject: JObject.Parse(File.ReadAllText(x.File)), x.Profile)) + .Reverse(); - IEnumerable ConvertToArguments(string profile, string name, string[] values) - { - var member = parameterMembers.SingleOrDefault(x => ParameterService.GetParameterMemberName(x).EqualsOrdinalIgnoreCase(name)); - var scalarType = member?.GetMemberType().GetScalarType(); - var mustDecrypt = (member?.HasCustomAttribute() ?? false) && !BuildServerConfigurationGeneration.IsActive; - var decryptedValues = values.Select(x => mustDecrypt ? DecryptValue(profile, name, x) : x); - var convertedValues = decryptedValues.Select(x => ConvertValue(scalarType, x)).ToList(); - Log.Verbose("Passing value for {Member} ({Value})", - member?.GetDisplayName() ?? "", - !mustDecrypt ? convertedValues.JoinComma() : "secret"); - return new[] { $"--{ParameterService.GetParameterDashedName(name)}" }.Concat(convertedValues); - } + var passwords = new Dictionary(); string DecryptValue(string profile, string name, string value) => EncryptionUtility.Decrypt( @@ -47,45 +65,24 @@ string DecryptValue(string profile, string name, string value) passwords[profile] = passwords.GetValueOrDefault(profile) ?? CredentialStore.GetPassword(profile, Build.RootDirectory), name); - // TODO: Abstract AbsolutePath/Solution/Project etc. - string ConvertValue([CanBeNull] Type scalarType, string value) - => typeof(IAbsolutePathHolder).IsAssignableFrom(scalarType) && - !PathConstruction.HasPathRoot(value) - ? EnvironmentInfo.WorkingDirectory.GetUnixRelativePathTo(Build.RootDirectory / value) - : value; - - var arguments = GetParameters().SelectMany(x => ConvertToArguments(x.Profile, x.Name, x.Values)).ToArray(); - ParameterService.Instance.ArgumentsFromFilesService = new ArgumentParser(arguments); - } + ParameterService.Instance.ArgumentsFromFilesService = (parameter, destinationType) => + { + var (property, profile) = jobjectsAndProfiles.Select(x => (Property: x.JObject.Property(parameter), x.Profile)) + .Where(x => x.Property != null) + .FirstOrDefault(); + if (property == null) + return null; - private IEnumerable<(string Profile, string Name, string[] Values)> GetParameters() - { - IEnumerable GetValues(JProperty property) - // TODO: if property is object || property is array && array contains objects => base64 - => property.Value is JArray array - ? array.Values() - : property.Values(); + var member = parameterMembers.SingleOrDefault(x => ParameterService.GetParameterMemberName(x).EqualsOrdinalIgnoreCase(parameter)); + var scalarType = member?.GetMemberType().GetScalarType(); + if (typeof(IAbsolutePathHolder).IsAssignableFrom(scalarType)) + return property.Value.ToObject().Apply(x => !PathConstruction.HasPathRoot(x) ? NukeBuild.RootDirectory / x : (AbsolutePath)x); - IEnumerable<(string Name, string[] Values)> Load(AbsolutePath file) - { - try - { - var jobject = JObject.Parse(file.ReadAllText()); - // TODO: use NukeBuild instance to match members and walk through structure to replace secrets and absolute-paths - return jobject.Properties() - .Where(x => x.Name != "$schema") - .Select(x => (x.Name, GetValues(x).ToArray())); - } - catch (Exception exception) - { - throw new Exception($"Failed parsing parameters file '{file}'.", exception); - } - } + if ((member?.HasCustomAttribute() ?? false) && + !BuildServerConfigurationGeneration.IsActive) + return DecryptValue(profile, parameter, property.Value.ToObject()); - return new[] { (File: Constants.GetDefaultParametersFile(Build.RootDirectory), Profile: Constants.DefaultProfileName) } - .Where(x => x.File.Exists()) - .Concat(Build.LoadedLocalProfiles.Select(x => (File: Constants.GetParametersProfileFile(Build.RootDirectory, x), Profile: x))) - .ForEachLazy(x => Assert.FileExists(x.File)) - .SelectMany(x => Load(x.File), (x, r) => (x.Profile, r.Name, r.Values)); + return property.Value.ToObject(destinationType); + }; } } diff --git a/source/Nuke.Build/Execution/Extensions/HandleShellCompletionAttribute.cs b/source/Nuke.Build/Execution/Extensions/HandleShellCompletionAttribute.cs index 152d031fe..0baf73f2f 100644 --- a/source/Nuke.Build/Execution/Extensions/HandleShellCompletionAttribute.cs +++ b/source/Nuke.Build/Execution/Extensions/HandleShellCompletionAttribute.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.Linq; using Nuke.Common.CI; +using Nuke.Common.IO; using Nuke.Common.Utilities; using static Nuke.Common.Constants; @@ -31,12 +32,23 @@ public void OnBuildCreated(IReadOnlyCollection executableTarge } else if (Build.BuildProjectFile != null) { - SchemaUtility.WriteBuildSchemaFile(Build); - SchemaUtility.WriteDefaultParametersFile(Build); + var buildSchema = SchemaUtility.GetJsonString(Build); + var buildSchemaFile = GetBuildSchemaFile(Build.RootDirectory); + buildSchemaFile.WriteAllText(buildSchema); + + var parametersFile = GetDefaultParametersFile(Build.RootDirectory); + if (!parametersFile.Exists()) + { + parametersFile.WriteAllText($$""" + { + "$schema": "./{{BuildSchemaFileName}}" + } + """); + } } else if (ParameterService.GetPositionalArgument(0) == ":complete") { - var schema = SchemaUtility.GetBuildSchema(Build); + var schema = SchemaUtility.GetJsonDocument(Build); var profileNames = GetProfileNames(Build.RootDirectory); var completionItems = CompletionUtility.GetItemsFromSchema(schema, profileNames); diff --git a/source/Nuke.Build/Execution/ParameterService.cs b/source/Nuke.Build/Execution/ParameterService.cs index b62071cc6..66bb1c718 100644 --- a/source/Nuke.Build/Execution/ParameterService.cs +++ b/source/Nuke.Build/Execution/ParameterService.cs @@ -17,7 +17,8 @@ namespace Nuke.Common; internal partial class ParameterService { - internal ArgumentParser ArgumentsFromFilesService; + // internal ArgumentParser ArgumentsFromFilesService; + internal Func ArgumentsFromFilesService; internal ArgumentParser ArgumentsFromCommitMessageService; private readonly Func _argumentParserProvider; @@ -154,7 +155,8 @@ object TryFromEnvironmentVariables() => // TODO: nuke ? object TryFromProfileArguments() => - ArgumentsFromFilesService?.GetNamedArgument(parameterName, destinationType, separator); + // ArgumentsFromFilesService?.GetNamedArgument(parameterName, destinationType, separator); + ArgumentsFromFilesService?.Invoke(parameterName, destinationType); object TryFromCommitMessageArguments() => ArgumentsFromCommitMessageService?.GetNamedArgument(parameterName, destinationType, separator); diff --git a/source/Nuke.Build/Nuke.Build.csproj b/source/Nuke.Build/Nuke.Build.csproj index f30534e9c..94c5801ed 100644 --- a/source/Nuke.Build/Nuke.Build.csproj +++ b/source/Nuke.Build/Nuke.Build.csproj @@ -17,12 +17,13 @@ - - - - - - + + + + + + + diff --git a/source/Nuke.Build/NukeBuild.Statics.cs b/source/Nuke.Build/NukeBuild.Statics.cs index 39157c7db..a400dd825 100644 --- a/source/Nuke.Build/NukeBuild.Statics.cs +++ b/source/Nuke.Build/NukeBuild.Statics.cs @@ -82,7 +82,7 @@ public static Verbosity Verbosity /// /// Gets the host for execution. Default is automatic. /// - [Parameter("Host for execution. Default is 'automatic'.", ValueProviderMember = nameof(HostNames))] + [Parameter("Host for execution. Default is 'automatic'.")] public static Host Host { get; set; } [Parameter("Defines the profiles to load.", Name = LoadedLocalProfilesParameterName)] diff --git a/source/Nuke.Build/NukeBuild.cs b/source/Nuke.Build/NukeBuild.cs index d4ca73bc3..6f9aa302d 100644 --- a/source/Nuke.Build/NukeBuild.cs +++ b/source/Nuke.Build/NukeBuild.cs @@ -87,8 +87,7 @@ protected static int Execute(params Expression>[] defaultTarg /// [Parameter("List of targets to be invoked. Default is '{default_target}'.", Name = InvokedTargetsParameterName, - Separator = TargetsSeparator, - ValueProviderMember = nameof(TargetNames))] + Separator = TargetsSeparator)] public IReadOnlyCollection InvokedTargets => ExecutionPlan.Where(x => x.Invoked).ToList(); /// @@ -96,8 +95,7 @@ protected static int Execute(params Expression>[] defaultTarg /// [Parameter("List of targets to be skipped. Empty list skips all dependencies.", Name = SkippedTargetsParameterName, - Separator = TargetsSeparator, - ValueProviderMember = nameof(TargetNames))] + Separator = TargetsSeparator)] public IReadOnlyCollection SkippedTargets => ExecutionPlan.Where(x => x.Status == ExecutionStatus.Skipped).ToList(); /// diff --git a/source/Nuke.Build/Utilities/SchemaUtility.cs b/source/Nuke.Build/Utilities/SchemaUtility.cs index 4d61f96d8..b5a7acbbf 100644 --- a/source/Nuke.Build/Utilities/SchemaUtility.cs +++ b/source/Nuke.Build/Utilities/SchemaUtility.cs @@ -5,9 +5,15 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Text.Encodings.Web; +using System.Reflection; using System.Text.Json; -using Nuke.Common.IO; +using Namotion.Reflection; +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; +using Newtonsoft.Json.Serialization; +using NJsonSchema; +using NJsonSchema.Generation; +using NuGet.Packaging; using Nuke.Common.Utilities; using Nuke.Common.ValueInjection; using static Nuke.Common.Constants; @@ -16,93 +22,146 @@ namespace Nuke.Common.Execution; public class SchemaUtility { - public static void WriteBuildSchemaFile(INukeBuild build) + private class SchemaGenerator : JsonSchemaGenerator { - var buildSchemaFile = GetBuildSchemaFile(build.RootDirectory); - var buildSchema = GetBuildSchema(build); - var options = new JsonSerializerOptions { WriteIndented = true, Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping }; - var json = JsonSerializer.Serialize(buildSchema, options); - buildSchemaFile.WriteAllText(json); - } + private class Resolver : DefaultContractResolver + { + protected override List GetSerializableMembers(Type objectType) + { + return objectType == typeof(ExecutableTarget) || objectType == typeof(Host) + ? new List() + : base.GetSerializableMembers(objectType); + } + } - // ReSharper disable once CognitiveComplexity - public static JsonDocument GetBuildSchema(INukeBuild build) - { - var parameters = ValueInjectionUtility - .GetParameterMembers(build.GetType(), includeUnlisted: true) - // .Where(x => x.DeclaringType != typeof(NukeBuild)) - .Select(x => - new + public static JsonSchema Generate(T build) where T : INukeBuild + { + return new SchemaGenerator( + build, + new JsonSchemaGeneratorSettings { - Name = ParameterService.GetParameterMemberName(x), - Description = ParameterService.GetParameterDescription(x), - MemberType = x.GetMemberType(), - ScalarType = x.GetMemberType().GetScalarType(), - EnumValues = ParameterService.GetParameterValueSet(x, build)?.Select(x => x.Text), - IsRequired = x.HasCustomAttribute(), - IsSecret = x.HasCustomAttribute() - }).ToList(); - - string GetJsonType(Type type) - => type.IsCollectionLike() - ? "array" - : type.GetScalarType() == typeof(int) - ? "integer" - : type.GetScalarType() == typeof(bool) - ? "boolean" - : "string"; - - var properties = new Dictionary(); - foreach (var parameter in parameters) + FlattenInheritanceHierarchy = true, + SerializerSettings = + new JsonSerializerSettings + { + ContractResolver = new Resolver(), + Converters = new JsonConverter[] { new StringEnumConverter() } + } + }).Generate(); + } + + private readonly INukeBuild _build; + + private SchemaGenerator(INukeBuild build, JsonSchemaGeneratorSettings settings) + : base(settings) + { + _build = build; + } + + private JsonSchema Generate() { - var property = new Dictionary(); - property["type"] = GetJsonType(parameter.MemberType); + var baseSchema = new JsonSchema(); + var userSchema = new JsonSchema(); + var schemaResolver = new JsonSchemaResolver(userSchema, Settings); + + var parameterMembers = ValueInjectionUtility.GetParameterMembers(_build.GetType(), includeUnlisted: true); + foreach (var parameterMember in parameterMembers) + { + var schema = parameterMember.DeclaringType == typeof(NukeBuild) ? baseSchema : userSchema; + var name = ParameterService.GetParameterMemberName(parameterMember); + var property = CreateProperty(parameterMember, schemaResolver); + schema.Properties[name] = property; + } + + // ValueInjectionUtility.GetParameterMembers(_build.GetType(), includeUnlisted: true) + // // .Where(x => x.Name.EqualsAnyOrdinalIgnoreCase( + // // nameof(NukeBuild.SkippedTargets), + // // nameof(NukeBuild.InvokedTargets), + // // nameof(NukeBuild.Verbosity) + // // )) + // .ToDictionary(ParameterService.GetParameterMemberName, x => CreateProperty(x, schemaResolver)) + // .ForEach(x => + // { + // baseSchema.Properties[x.Key] = x.Value; + // }); - if (parameter.Description != null) - property["description"] = parameter.Description; + userSchema.Reference = baseSchema; + userSchema.Definitions[nameof(NukeBuild)] = baseSchema; - if (parameter.IsSecret) - property["default"] = "Secrets must be entered via 'nuke :secrets [profile]'"; + // TODO: why can't this use value sets? + var targetNames = ExecutableTargetFactory.GetTargetProperties(_build.GetType()).Select(x => x.GetDisplayShortName()).OrderBy(x => x); + var executableTargetSchema = UpdatePropertySchema(nameof(ExecutableTarget), targetNames); + baseSchema.Properties[InvokedTargetsParameterName].Item = + baseSchema.Properties[SkippedTargetsParameterName].Item = new JsonSchema { Reference = executableTargetSchema }; - if (parameter.EnumValues != null && !parameter.MemberType.IsCollectionLike()) - property["enum"] = parameter.EnumValues; + var hostNames = Host.AvailableTypes.Select(x => x.Name).OrderBy(x => x); + var hostSchema = UpdatePropertySchema(nameof(NukeBuild.Host), hostNames); + baseSchema.Properties[nameof(NukeBuild.Host)].Reference = hostSchema; - if (parameter.MemberType.IsCollectionLike()) + RemoveXEnumValues(); + + return userSchema; + + JsonSchema UpdatePropertySchema(string name, IEnumerable values) { - var items = new Dictionary(); - items["type"] = GetJsonType(parameter.ScalarType); - if (parameter.EnumValues != null) - items["enum"] = parameter.EnumValues; - property["items"] = items; + var schema = userSchema.Definitions[name]; + schema.Type = JsonObjectType.String; + schema.AllowAdditionalProperties = true; + schema.Enumeration.AddRange(values); + return schema; } - properties[parameter.Name] = property; + void RemoveXEnumValues() + { + foreach (var definition in userSchema.Definitions.Values) + { + definition.EnumerationNames.Clear(); + definition.AllowAdditionalProperties = true; + } + } } - var build2 = new Dictionary { ["type"] = "object", ["properties"] = properties }; - var definitions = new Dictionary { ["build"] = build2 }; - var jsonDictionary = new Dictionary - { - ["$schema"] = "http://json-schema.org/draft-04/schema#", - ["$ref"] = "#/definitions/build", - ["title"] = "Build Schema", - ["definitions"] = definitions - }; - return JsonDocument.Parse(JsonSerializer.Serialize(jsonDictionary)); + private JsonSchemaProperty CreateProperty(MemberInfo parameterMember, JsonSchemaResolver schemaResolver) + { + var property = parameterMember.GetCustomAttribute().NotNull().GetType() == typeof(ParameterAttribute) + ? GenerateWithReference( + parameterMember.ToContextualAccessor().AccessorType, + schemaResolver) + : new JsonSchemaProperty { Type = JsonObjectType.String }; + + property.Description = ParameterService.GetParameterDescription(parameterMember); + property.Default = parameterMember.HasCustomAttribute() + ? "Secrets must be entered via 'nuke :secrets [profile]'" + : null; + + var values = ParameterService.GetParameterValueSet(parameterMember, _build) + ?.Select(x => (object)x.Text); + if (values != null && !parameterMember.GetMemberType().IsEnum) + { + property.Type = !parameterMember.GetMemberType().IsCollectionLike() + ? JsonObjectType.String + : JsonObjectType.Array; + var propertySchema = property.Reference ?? property; + if (property.Type == JsonObjectType.String) + propertySchema.Enumeration.AddRange(values); + else + propertySchema.Item.Enumeration.AddRange(values); + } + + if (Nullable.GetUnderlyingType(parameterMember.GetMemberType()) != null) + property.Type |= JsonObjectType.Null; + + return property; + } } - public static void WriteDefaultParametersFile(INukeBuild build) + public static string GetJsonString(INukeBuild build) { - var parametersFile = GetDefaultParametersFile(build.RootDirectory); - if (parametersFile.Exists()) - return; + return SchemaGenerator.Generate(build).ToJson(); + } - parametersFile.WriteAllLines( - new[] - { - "{", - $" \"$schema\": \"./{BuildSchemaFileName}\"", - "}" - }); + public static JsonDocument GetJsonDocument(INukeBuild build) + { + return JsonDocument.Parse(GetJsonString(build)); } } diff --git a/source/Nuke.Common.Tests/CI/ConfigurationGenerationTest.cs b/source/Nuke.Common.Tests/CI/ConfigurationGenerationTest.cs index c8f5c3146..4fe4eb487 100644 --- a/source/Nuke.Common.Tests/CI/ConfigurationGenerationTest.cs +++ b/source/Nuke.Common.Tests/CI/ConfigurationGenerationTest.cs @@ -77,8 +77,7 @@ public class TestBuild : NukeBuild ( null, new TestAzurePipelinesAttribute( - AzurePipelinesImage.Ubuntu2204, - AzurePipelinesImage.Windows2019) + AzurePipelinesImage.Ubuntu2204) { NonEntryTargets = new[] { nameof(Clean) }, InvokedTargets = new[] { nameof(Test) }, diff --git a/source/Nuke.Common.Tests/CI/TestAzurePipelinesAttribute.cs b/source/Nuke.Common.Tests/CI/TestAzurePipelinesAttribute.cs index 50fc46b50..a98147077 100644 --- a/source/Nuke.Common.Tests/CI/TestAzurePipelinesAttribute.cs +++ b/source/Nuke.Common.Tests/CI/TestAzurePipelinesAttribute.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 @@ -11,8 +11,8 @@ namespace Nuke.Common.Tests.CI; public class TestAzurePipelinesAttribute : AzurePipelinesAttribute, ITestConfigurationGenerator { - public TestAzurePipelinesAttribute(AzurePipelinesImage image, params AzurePipelinesImage[] images) - : base(image, images) + public TestAzurePipelinesAttribute(AzurePipelinesImage image) + : base(image) { } diff --git a/source/Nuke.Common/CI/AzurePipelines/AzurePipelinesAttribute.cs b/source/Nuke.Common/CI/AzurePipelines/AzurePipelinesAttribute.cs index 05b09523d..5a62c597a 100644 --- a/source/Nuke.Common/CI/AzurePipelines/AzurePipelinesAttribute.cs +++ b/source/Nuke.Common/CI/AzurePipelines/AzurePipelinesAttribute.cs @@ -22,7 +22,7 @@ namespace Nuke.Common.CI.AzurePipelines; public class AzurePipelinesAttribute : ChainedConfigurationAttributeBase { private readonly string _suffix; - private readonly AzurePipelinesImage[] _images; + private readonly AzurePipelinesPool _pool; private bool? _triggerBatch; private bool? _pullRequestsAutoCancel; @@ -32,19 +32,39 @@ public class AzurePipelinesAttribute : ChainedConfigurationAttributeBase private bool? _clean; public AzurePipelinesAttribute( - AzurePipelinesImage image, - params AzurePipelinesImage[] images) - : this(suffix: null, image, images) + AzurePipelinesImage image) + : this(suffix: null, image) { } public AzurePipelinesAttribute( [CanBeNull] string suffix, - AzurePipelinesImage image, - params AzurePipelinesImage[] images) + AzurePipelinesImage image) + : this(suffix, new AzurePipelinesPool(image)) + { + } + + public AzurePipelinesAttribute( + string poolName, + params string[] demands) + : this(suffix: null, poolName, demands) + { + } + + public AzurePipelinesAttribute( + [CanBeNull] string suffix, + string poolName, + params string[] demands) + : this(suffix, new AzurePipelinesPool(poolName, demands)) + { + } + + private AzurePipelinesAttribute( + [CanBeNull] string suffix, + AzurePipelinesPool pool) { _suffix = suffix?.Replace(oldChar: ' ', newChar: '_'); - _images = new[] { image }.Concat(images).ToArray(); + _pool = pool; } public override string IdPostfix => _suffix; @@ -126,12 +146,12 @@ public override CustomFileWriter CreateWriter(StreamWriter streamWriter) public override ConfigurationEntity GetConfiguration(IReadOnlyCollection relevantTargets) { return new AzurePipelinesConfiguration - { - VariableGroups = ImportVariableGroups, - VcsPushTrigger = GetVcsPushTrigger(), - VcsPullRequestTrigger = GetVcsPullRequestTrigger(), - Stages = _images.Select(x => GetStage(x, relevantTargets)).ToArray() - }; + { + VariableGroups = ImportVariableGroups, + VcsPushTrigger = GetVcsPushTrigger(), + VcsPullRequestTrigger = GetVcsPullRequestTrigger(), + Stages = [GetStage(_pool, relevantTargets)] + }; } [CanBeNull] @@ -148,16 +168,16 @@ protected AzurePipelinesVcsPushTrigger GetVcsPushTrigger() return null; return new AzurePipelinesVcsPushTrigger - { - Disabled = TriggerDisabled, - Batch = _triggerBatch, - BranchesInclude = TriggerBranchesInclude, - BranchesExclude = TriggerBranchesExclude, - TagsInclude = TriggerTagsInclude, - TagsExclude = TriggerTagsExclude, - PathsInclude = TriggerPathsInclude, - PathsExclude = TriggerPathsExclude, - }; + { + Disabled = TriggerDisabled, + Batch = _triggerBatch, + BranchesInclude = TriggerBranchesInclude, + BranchesExclude = TriggerBranchesExclude, + TagsInclude = TriggerTagsInclude, + TagsExclude = TriggerTagsExclude, + PathsInclude = TriggerPathsInclude, + PathsExclude = TriggerPathsExclude, + }; } [CanBeNull] @@ -172,70 +192,71 @@ protected AzurePipelinesVcsPushTrigger GetVcsPullRequestTrigger() return null; return new AzurePipelinesVcsPushTrigger - { - Disabled = PullRequestsDisabled, - AutoCancel = _pullRequestsAutoCancel, - BranchesInclude = PullRequestsBranchesInclude, - BranchesExclude = PullRequestsBranchesExclude, - TagsInclude = new string[0], - TagsExclude = new string[0], - PathsInclude = PullRequestsPathsInclude, - PathsExclude = PullRequestsPathsExclude, - }; + { + Disabled = PullRequestsDisabled, + AutoCancel = _pullRequestsAutoCancel, + BranchesInclude = PullRequestsBranchesInclude, + BranchesExclude = PullRequestsBranchesExclude, + TagsInclude = new string[0], + TagsExclude = new string[0], + PathsInclude = PullRequestsPathsInclude, + PathsExclude = PullRequestsPathsExclude, + }; } protected virtual AzurePipelinesStage GetStage( - AzurePipelinesImage image, + AzurePipelinesPool pool, IReadOnlyCollection relevantTargets) { var lookupTable = new LookupTable(); var jobs = relevantTargets - .Select(x => (ExecutableTarget: x, Job: GetJob(x, lookupTable, relevantTargets, image))) + .Select(x => (ExecutableTarget: x, Job: GetJob(x, lookupTable, relevantTargets, pool))) .ForEachLazy(x => lookupTable.Add(x.ExecutableTarget, x.Job)) .Select(x => x.Job).ToArray(); return new AzurePipelinesStage - { - Name = image.GetValue().Replace("-", "_").Replace(".", "_"), - DisplayName = image.GetValue(), - Image = image, - Dependencies = new AzurePipelinesStage[0], - Jobs = jobs - }; + { + Name = pool.GetNameForStage().Replace("-", "_").Replace(".", "_"), + DisplayName = pool.GetNameForStage(), + Pool = pool, + Dependencies = new AzurePipelinesStage[0], + Jobs = jobs + }; } protected virtual AzurePipelinesJob GetJob( ExecutableTarget executableTarget, LookupTable jobs, IReadOnlyCollection relevantTargets, - AzurePipelinesImage image) + AzurePipelinesPool pool) { var totalPartitions = executableTarget.PartitionSize ?? 0; var dependencies = GetTargetDependencies(executableTarget).SelectMany(x => jobs[x]).ToArray(); return new AzurePipelinesJob - { - Name = executableTarget.Name, - DisplayName = executableTarget.Name, - Dependencies = dependencies, - Parallel = totalPartitions, - Steps = GetSteps(executableTarget, relevantTargets, image).ToArray(), - }; + { + Name = executableTarget.Name, + DisplayName = executableTarget.Name, + Dependencies = dependencies, + Parallel = totalPartitions, + Steps = GetSteps(executableTarget, relevantTargets, pool).ToArray(), + Pool = pool + }; } protected virtual IEnumerable GetSteps( ExecutableTarget executableTarget, IReadOnlyCollection relevantTargets, - AzurePipelinesImage image) + AzurePipelinesPool pool) { if (_submodules.HasValue || _largeFileStorage.HasValue || _fetchDepth.HasValue || _clean.HasValue) { yield return new AzurePipelineCheckoutStep - { - InclueSubmodules = _submodules, - IncludeLargeFileStorage = _largeFileStorage, - FetchDepth = _fetchDepth, - Clean = _clean - }; + { + InclueSubmodules = _submodules, + IncludeLargeFileStorage = _largeFileStorage, + FetchDepth = _fetchDepth, + Clean = _clean + }; } if (CacheKeyFiles.Any()) @@ -243,11 +264,11 @@ protected virtual IEnumerable GetSteps( foreach (var cachePath in CachePaths.NotNull()) { yield return new AzurePipelinesCacheStep - { - Image = image, - KeyFiles = CacheKeyFiles, - Path = cachePath - }; + { + Pool = pool, + KeyFiles = CacheKeyFiles, + Path = cachePath + }; } } @@ -257,7 +278,7 @@ string GetArtifactPath(AbsolutePath path) : path; var publishedArtifacts = executableTarget.ArtifactProducts - .Select(x => (AbsolutePath) x) + .Select(x => (AbsolutePath)x) .Select(x => x.DescendantsAndSelf(y => y.Parent).FirstOrDefault(y => !y.ToString().ContainsOrdinalIgnoreCase("*"))) .Distinct() .Select(GetArtifactPath).ToArray(); @@ -277,21 +298,21 @@ string GetArtifactPath(AbsolutePath path) var chainLinkTargets = GetInvokedTargets(executableTarget, relevantTargets).ToArray(); yield return new AzurePipelinesCmdStep - { - BuildCmdPath = BuildCmdPath, - PartitionSize = executableTarget.PartitionSize, - InvokedTargets = chainLinkTargets.Select(x => x.Name).ToArray(), - Imports = GetImports().ToDictionary(x => x.Key, x => x.Value) - }; + { + BuildCmdPath = BuildCmdPath, + PartitionSize = executableTarget.PartitionSize, + InvokedTargets = chainLinkTargets.Select(x => x.Name).ToArray(), + Imports = GetImports().ToDictionary(x => x.Key, x => x.Value) + }; foreach (var publishedArtifact in publishedArtifacts) { var artifactName = publishedArtifact.Split('/').Last(); yield return new AzurePipelinesPublishStep - { - ArtifactName = artifactName, - PathToPublish = publishedArtifact - }; + { + ArtifactName = artifactName, + PathToPublish = publishedArtifact + }; } } @@ -313,6 +334,6 @@ protected virtual string GetArtifact(string artifact) return HasPathRoot(artifact) ? artifact - : (UnixRelativePath) artifact; + : (UnixRelativePath)artifact; } } diff --git a/source/Nuke.Common/CI/AzurePipelines/Configuration/AzurePipelinesCacheStep.cs b/source/Nuke.Common/CI/AzurePipelines/Configuration/AzurePipelinesCacheStep.cs index bdd1deff5..0f018565b 100644 --- a/source/Nuke.Common/CI/AzurePipelines/Configuration/AzurePipelinesCacheStep.cs +++ b/source/Nuke.Common/CI/AzurePipelines/Configuration/AzurePipelinesCacheStep.cs @@ -14,14 +14,12 @@ namespace Nuke.Common.CI.AzurePipelines.Configuration; [PublicAPI] public class AzurePipelinesCacheStep : AzurePipelinesStep { - public AzurePipelinesImage Image { get; set; } + public AzurePipelinesPool Pool { get; set; } public string[] KeyFiles { get; set; } public string Path { get; set; } private string AdjustedPath => - Image.GetValue().StartsWithAnyOrdinalIgnoreCase("ubuntu", "macos") - ? Path.Replace("~", "$(HOME)") - : Path.Replace("~", "$(USERPROFILE)"); + Path.Replace("~", Pool.GetUserHomeDirectory()); private string Identifier => Path .Replace(".", "/") diff --git a/source/Nuke.Common/CI/AzurePipelines/Configuration/AzurePipelinesJob.cs b/source/Nuke.Common/CI/AzurePipelines/Configuration/AzurePipelinesJob.cs index 89fc49481..94343ab48 100644 --- a/source/Nuke.Common/CI/AzurePipelines/Configuration/AzurePipelinesJob.cs +++ b/source/Nuke.Common/CI/AzurePipelines/Configuration/AzurePipelinesJob.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,7 +16,7 @@ public class AzurePipelinesJob : ConfigurationEntity { public string Name { get; set; } public string DisplayName { get; set; } - public AzurePipelinesImage? Image { get; set; } + public AzurePipelinesPool Pool { get; set; } public AzurePipelinesJob[] Dependencies { get; set; } public int Parallel { get; set; } public AzurePipelinesStep[] Steps { get; set; } @@ -28,12 +28,9 @@ public override void Write(CustomFileWriter writer) writer.WriteLine($"displayName: {DisplayName.SingleQuote()}"); writer.WriteLine($"dependsOn: [ {Dependencies.Select(x => x.Name).JoinCommaSpace()} ]"); - if (Image != null) + if (Pool != null) { - using (writer.WriteBlock("pool:")) - { - writer.WriteLine($"vmImage: {Image.Value.GetValue().SingleQuote().SingleQuote()}"); - } + Pool.Write(writer); } if (Parallel > 1) diff --git a/source/Nuke.Common/CI/AzurePipelines/Configuration/AzurePipelinesPool.cs b/source/Nuke.Common/CI/AzurePipelines/Configuration/AzurePipelinesPool.cs new file mode 100644 index 000000000..0d3bfa8c5 --- /dev/null +++ b/source/Nuke.Common/CI/AzurePipelines/Configuration/AzurePipelinesPool.cs @@ -0,0 +1,94 @@ +using JetBrains.Annotations; +using Nuke.Common.Tooling; +using Nuke.Common.Utilities; +using Nuke.Common.Utilities.Collections; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Security; +using System.Text; +using System.Threading.Tasks; + +namespace Nuke.Common.CI.AzurePipelines.Configuration +{ + [PublicAPI] + public class AzurePipelinesPool : ConfigurationEntity + { + public AzurePipelinesPool(AzurePipelinesImage image) + { + Image = image; + } + + public AzurePipelinesPool(string name, params string[] demands) + { + Name = name; + Demands = demands; + } + + public string Name { get; } + public string[] Demands { get; } + public AzurePipelinesImage? Image { get; } + + public string GetNameForStage() + { + if (Image != null) + { + return Image.Value.GetValue(); + } + else + { + return Name; + } + } + + public override void Write(CustomFileWriter writer) + { + using (writer.WriteBlock("pool:")) + { + if (Image != null) + { + writer.WriteLine($"vmImage: {Image.Value.GetValue().SingleQuote().SingleQuote()}"); + } + else if (Name != null) + { + writer.WriteLine($"name: {Name}"); + + if (Demands?.Any() == true) + { + using (writer.WriteBlock("demands:")) + { + Demands.ForEach(x => writer.WriteLine($"- {x}")); + } + } + } + } + } + + public string GetUserHomeDirectory() + { + if (Image != null) + { + return GetUserHomeDirectory(Image.Value.GetValue()); + } + else + { + var agentOS = Demands?.FirstOrDefault(x => x.StartsWith("Agent.OS")); + if (agentOS != null) + { + return GetUserHomeDirectory(agentOS.SplitSpace().Last()); + } + } + + // By default, we assume it's Linux based pool + return GetUserHomeDirectory("ubuntu"); + } + + private static string GetUserHomeDirectory(string os) + { + return os.StartsWithAnyOrdinalIgnoreCase("ubuntu", "macos") + ? "$(HOME)" + : "$(USERPROFILE)"; + } + } +} diff --git a/source/Nuke.Common/CI/AzurePipelines/Configuration/AzurePipelinesStage.cs b/source/Nuke.Common/CI/AzurePipelines/Configuration/AzurePipelinesStage.cs index d85cc2cb0..9b3adc8d9 100644 --- a/source/Nuke.Common/CI/AzurePipelines/Configuration/AzurePipelinesStage.cs +++ b/source/Nuke.Common/CI/AzurePipelines/Configuration/AzurePipelinesStage.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,7 +16,7 @@ public class AzurePipelinesStage : ConfigurationEntity { public string Name { get; set; } public string DisplayName { get; set; } - public AzurePipelinesImage? Image { get; set; } + public AzurePipelinesPool Pool { get; set; } public AzurePipelinesStage[] Dependencies { get; set; } public AzurePipelinesJob[] Jobs { get; set; } @@ -27,12 +27,9 @@ public override void Write(CustomFileWriter writer) writer.WriteLine($"displayName: {DisplayName.SingleQuote()}"); writer.WriteLine($"dependsOn: [ {Dependencies.Select(x => x.Name).JoinCommaSpace()} ]"); - if (Image != null) + if (Pool != null) { - using (writer.WriteBlock("pool:")) - { - writer.WriteLine($"vmImage: {Image.Value.GetValue().SingleQuote()}"); - } + Pool.Write(writer); } using (writer.WriteBlock("jobs:")) diff --git a/source/Nuke.Common/Nuke.Common.csproj b/source/Nuke.Common/Nuke.Common.csproj index 006c19995..0ab89aea1 100644 --- a/source/Nuke.Common/Nuke.Common.csproj +++ b/source/Nuke.Common/Nuke.Common.csproj @@ -23,11 +23,11 @@ - - - - - + + + + + diff --git a/source/Nuke.Common/Tools/Codecov/Codecov.json b/source/Nuke.Common/Tools/Codecov/Codecov.json index 08b9b6cb0..2f70d769d 100644 --- a/source/Nuke.Common/Tools/Codecov/Codecov.json +++ b/source/Nuke.Common/Tools/Codecov/Codecov.json @@ -6,7 +6,7 @@ "name": "Codecov", "officialUrl": "https://about.codecov.io/", "help": "Code coverage is a measurement used to express which lines of code were executed by a test suite. We use three primary terms to describe each line executed.
  • hit - indicates that the source code was executed by the test suite.
  • partial - indicates that the source code was not fully executed by the test suite; there are remaining branches that were not executed.
  • miss - indicates that the source code was not executed by the test suite.
Coverage is the ratio of hits / (sum of hit + partial + miss). A code base that has 5 lines executed by tests out of 12 total lines will receive a coverage ratio of 41% (rounding down).Phrased simply, code coverage provides a visual measurement of what source code is being executed by a test suite. This information indicates to the software developer where they should write new tests in an effort to achieve higher coverage.Testing source code helps to prevent bugs and syntax errors by executing each line with a known variable and cross-checking it with an expected output.", - "nugetPackageId": "Codecov.Tool", + "nugetPackageId": "CodecovUploader", "customExecutable": true, "tasks": [ { diff --git a/source/Nuke.Common/Tools/Codecov/CodecovTasks.cs b/source/Nuke.Common/Tools/Codecov/CodecovTasks.cs index b63f68263..1ca823c87 100644 --- a/source/Nuke.Common/Tools/Codecov/CodecovTasks.cs +++ b/source/Nuke.Common/Tools/Codecov/CodecovTasks.cs @@ -2,6 +2,7 @@ // Distributed under the MIT License. // https://github.com/nuke-build/nuke/blob/master/LICENSE +using System; using Nuke.Common.Tooling; namespace Nuke.Common.Tools.Codecov; @@ -10,17 +11,27 @@ partial class CodecovSettings { private string GetProcessToolPath() { - return CodecovTasks.GetToolPath(Framework); + return CodecovTasks.GetToolPath(); } } partial class CodecovTasks { - internal static string GetToolPath(string framework = null) + internal static string GetToolPath() { return NuGetToolPathResolver.GetPackageExecutable( - packageId: "Codecov.Tool", - packageExecutable: "codecov.dll", - framework: framework); + packageId: "CodecovUploader", + packageExecutable: GetPackageExecutable()); + } + + private static string GetPackageExecutable() + { + return EnvironmentInfo.Platform switch + { + PlatformFamily.Windows => "codecov.exe", + PlatformFamily.OSX => "codecov-macos", + PlatformFamily.Linux => "codecov-linux", + _ => throw new ArgumentOutOfRangeException() + }; } } diff --git a/source/Nuke.Common/Tools/DotNet/DotNet.json b/source/Nuke.Common/Tools/DotNet/DotNet.json index e59216b7c..6b94c37d6 100644 --- a/source/Nuke.Common/Tools/DotNet/DotNet.json +++ b/source/Nuke.Common/Tools/DotNet/DotNet.json @@ -690,6 +690,12 @@ "format": "{value}", "help": "The project to publish, which defaults to the current directory if not specified." }, + { + "name": "Architecture", + "type": "string", + "format": "--arch {value}", + "help": "Specifies the target architecture. This is a shorthand syntax for setting the Runtime Identifier (RID), where the provided value is combined with the default RID. For example, on a win-x64 machine, specifying --arch x86 sets the RID to win-x86. If you use this option, don't use the -r|--runtime option. Available since .NET 6 Preview 7." + }, { "name": "Configuration", "type": "string", @@ -726,6 +732,12 @@ "format": "--output {value}", "help": "Specifies the path for the output directory. If not specified, it defaults to ./bin/[configuration]/[framework]/ for a framework-dependent deployment or ./bin/[configuration]/[framework]/[runtime] for a self-contained deployment.If a relative path is provided, the output directory generated is relative to the project file location, not to the current working directory." }, + { + "name": "OperatingSystem", + "type": "string", + "format": "--os {value}", + "help": "Specifies the target operating system (OS). This is a shorthand syntax for setting the Runtime Identifier (RID), where the provided value is combined with the default RID. For example, on a win-x64 machine, specifying --os linux sets the RID to linux-x64. If you use this option, don't use the -r|--runtime option. Available since .NET 6." + }, { "name": "SelfContained", "type": "bool", @@ -755,6 +767,13 @@ "type": "bool", "format": "--nologo", "help": "Doesn't display the startup banner or the copyright message. Available since .NET Core 3.0 SDK." + }, + { + "name": "Targets", + "type": "List", + "format": "/t:{value}", + "separator": ";", + "help": "

Build the specified targets in the project. Specify each target separately, or use a semicolon or comma to separate multiple targets, as the following example shows:
/target:Resources;Compile

If you specify any targets by using this switch, they are run instead of any targets in the DefaultTargets attribute in the project file. For more information, see Target Build Order and How to: Specify Which Target to Build First.

A target is a group of tasks. For more information, see Targets.

" } ] } diff --git a/source/Nuke.Common/Tools/DotnetPackaging/DotnetPackaging.json b/source/Nuke.Common/Tools/DotnetPackaging/DotnetPackaging.json new file mode 100644 index 000000000..6bcad28ce --- /dev/null +++ b/source/Nuke.Common/Tools/DotnetPackaging/DotnetPackaging.json @@ -0,0 +1,264 @@ +{ + "$schema": "https://raw.githubusercontent.com/nuke-build/nuke/master/source/Nuke.Tooling.Generator/schema.json", + "references": [ + "https://github.com/SuperJMN/DotnetPackaging/tree/master/src/DotnetPackaging.Console" + ], + "name": "DotnetPackaging", + "officialUrl": "https://github.com/superjmn/dotnetpackaging", + "help": "DotnetPackaging is able to package your application into various formats, including Deb and AppImage.", + "nugetPackageId": "DotnetPackaging.Console", + "packageExecutable": "DotnetPackaging.Console.dll", + "tasks": [ + { + "help": "Creates a Debian package from the specified directory.", + "postfix": "Deb", + "definiteArgument": "deb", + "settingsClass": { + "properties": [ + { + "name": "Directory", + "type": "string", + "format": "--directory={value}", + "help": "The input directory from which to create the package." + }, + { + "name": "Metadata", + "type": "string", + "format": "--metadata={value}", + "help": "The metadata file to include in the package." + }, + { + "name": "Output", + "type": "string", + "format": "--output={value}", + "help": "The output DEB file to create." + } + ] + } + }, + { + "help": "Creates an AppImage package.", + "postfix": "AppImage", + "definiteArgument": "appimage", + "settingsClass": { + "properties": [ + { + "name": "Directory", + "type": "string", + "format": "--directory={value}", + "help": "The input directory from which to create the AppImage." + }, + { + "name": "Output", + "type": "string", + "format": "--output={value}", + "help": "The output AppImage file to create." + }, + { + "name": "ApplicationName", + "type": "string", + "format": "--application-name={value}", + "help": "The name of the application for the AppImage." + }, + { + "name": "MainCategory", + "type": "DotnetPackagingMainCategory", + "format": "--main-category {value}", + "help": "Main category of the application." + }, + { + "name": "AdditionalCategories", + "type": "List", + "format": "--additional-categories {value}", + "help": "Additional categories for the application." + }, + { + "name": "Icon", + "type": "string", + "format": "--icon {value}", + "help": "The icon path for the application. When not provided, the tool looks up for an image called AppImage.png." + }, + { + "name": "Homepage", + "type": "string", + "format": "--homepage {value}", + "help": "Home page of the application." + }, + { + "name": "License", + "type": "string", + "format": "--license {value}", + "help": "License of the application." + }, + { + "name": "Version", + "type": "string", + "format": "--version {value}", + "help": "Version of the application." + }, + { + "name": "ScreenshotUrls", + "type": "List", + "format": "--screenshot-urls {value}", + "help": "URLs of screenshots of the application." + }, + { + "name": "Summary", + "type": "string", + "format": "--summary {value}", + "help": "Short description of the application." + }, + { + "name": "AppId", + "type": "string", + "format": "--appId {value}", + "help": "Application ID, usually a reverse DNS name like com.SomeCompany.SomeApplication." + } + ] + } + } + ], + "enumerations": [ + { + "name": "DotnetPackagingMainCategory", + "values": [ + "AudioVideo", + "Audio", + "Video", + "Development", + "Education", + "Game", + "Graphics", + "Network", + "Office", + "Settings", + "Utility" + ] + }, + { + "name": "DotnetPackagingAdditionalCategory", + "values": [ + "Building", + "Debugger", + "IDE", + "GUIDesigner", + "Profiling", + "RevisionControl", + "Translation", + "Calendar", + "ContactManagement", + "Database", + "Dictionary", + "Chart", + "Email", + "Finance", + "FlowChart", + "PDA", + "ProjectManagement", + "Presentation", + "Spreadsheet", + "WordProcessor", + "TwoDGraphics", + "VectorGraphics", + "RasterGraphics", + "ThreeDGraphics", + "Scanning", + "OCR", + "Photography", + "Publishing", + "Viewer", + "TextTools", + "DesktopSettings", + "HardwareSettings", + "Printing", + "PackageManager", + "Dialup", + "InstantMessaging", + "Chat", + "IRCClient", + "FileTransfer", + "HamRadio", + "News", + "P2P", + "RemoteAccess", + "Telephony", + "TelephonyTools", + "VideoConference", + "WebBrowser", + "WebDevelopment", + "Midi", + "Mixer", + "Sequencer", + "Tuner", + "TV", + "AudioVideoEditing", + "Player", + "Recorder", + "DiscBurning", + "ActionGame", + "AdventureGame", + "ArcadeGame", + "BoardGame", + "BlocksGame", + "CardGame", + "KidsGame", + "LogicGame", + "RolePlaying", + "Simulation", + "SportsGame", + "StrategyGame", + "Art", + "Construction", + "Music", + "Languages", + "Science", + "ArtificialIntelligence", + "Astronomy", + "Biology", + "Chemistry", + "ComputerScience", + "DataVisualization", + "Economy", + "Electricity", + "Geography", + "Geology", + "Geoscience", + "History", + "ImageProcessing", + "Literature", + "Math", + "NumericalAnalysis", + "MedicalSoftware", + "Physics", + "Robotics", + "Sports", + "ParallelComputing", + "Amusement", + "Archiving", + "Compression", + "Electronics", + "Emulator", + "Engineering", + "FileTools", + "FileManager", + "TerminalEmulator", + "Filesystem", + "Monitor", + "Security", + "Accessibility", + "Calculator", + "Clock", + "TextEditor", + "Documentation", + "Core", + "KDE", + "GNOME", + "GTK", + "Qt", + "Motif", + "Java", + "ConsoleOnly" + ] + } + ] +} diff --git a/source/Nuke.Components/ISignPackages.cs b/source/Nuke.Components/ISignPackages.cs index c0d288579..54a17a250 100644 --- a/source/Nuke.Components/ISignPackages.cs +++ b/source/Nuke.Components/ISignPackages.cs @@ -52,16 +52,19 @@ public interface ISignPackages : INukeBuild { public const string SignPath = nameof(SignPath); + public record SignPathSettings( + string OrganizationId, + string ProjectSlug, + string PolicySlug); + + [Parameter] SignPathSettings Settings => TryGetValue(() => Settings); [Parameter] [Secret] string ApiToken => TryGetValue(() => ApiToken); - [Parameter] string OrganizationId => TryGetValue(() => OrganizationId); - [Parameter] string ProjectSlug => TryGetValue(() => ProjectSlug); - [Parameter] string PolicySlug => TryGetValue(() => PolicySlug); AbsolutePath SignPathTemporaryDirectory => TemporaryDirectory / "signpath"; AbsolutePath SignPathRequestDirectory => SignPathTemporaryDirectory / "signing-request"; AbsolutePath SignPathResponseDirectory => SignPathTemporaryDirectory / "signing-response"; - AbsolutePath SignPathRequestArchive => Path.ChangeExtension(SignPathRequestDirectory, ".zip"); - AbsolutePath SignPathResponseArchive => Path.ChangeExtension(SignPathResponseDirectory, ".zip"); + AbsolutePath SignPathRequestArchive => SignPathRequestDirectory.WithExtension(".zip"); + AbsolutePath SignPathResponseArchive => SignPathResponseDirectory.WithExtension(".zip"); IEnumerable SignPathPackages { get; } bool SignPathReplacePackages => true; @@ -72,10 +75,8 @@ public interface ISignPackages : INukeBuild .TryDependsOn() .TryDependentFor() .OnlyWhenStatic(() => AppVeyor != null) + .Requires(() => Settings) .Requires(() => ApiToken) - .Requires(() => OrganizationId) - .Requires(() => ProjectSlug) - .Requires(() => PolicySlug) .Executes(async () => { SignPathRequestDirectory.CreateOrCleanDirectory(); @@ -86,9 +87,9 @@ public interface ISignPackages : INukeBuild var signingRequestUrl = await GetSigningRequestUrlViaAppVeyor( ApiToken, - OrganizationId, - ProjectSlug, - PolicySlug); + Settings.OrganizationId, + Settings.ProjectSlug, + Settings.PolicySlug); ReportSummary(_ => _ .AddPair("Approve/Deny Request", signingRequestUrl.Replace("api/v1", "Web"))); diff --git a/source/Nuke.GlobalTool/Nuke.GlobalTool.csproj b/source/Nuke.GlobalTool/Nuke.GlobalTool.csproj index 0ecfb735b..a47a42bc8 100644 --- a/source/Nuke.GlobalTool/Nuke.GlobalTool.csproj +++ b/source/Nuke.GlobalTool/Nuke.GlobalTool.csproj @@ -14,10 +14,10 @@
- - - - + + + + diff --git a/source/Nuke.GlobalTool/Program.Setup.cs b/source/Nuke.GlobalTool/Program.Setup.cs index 4804be675..9700f1a37 100644 --- a/source/Nuke.GlobalTool/Program.Setup.cs +++ b/source/Nuke.GlobalTool/Program.Setup.cs @@ -245,7 +245,7 @@ void MakeExecutable(AbsolutePath scriptFile) private static void WriteConfigurationFile(AbsolutePath rootDirectory, [CanBeNull] AbsolutePath solutionFile) { var parametersFile = GetDefaultParametersFile(rootDirectory); - var dictionary = new Dictionary { ["$schema"] = $"./{BuildSchemaFileName}" }; + var dictionary = new Dictionary { ["$schema"] = BuildSchemaFileName }; if (solutionFile != null) dictionary["Solution"] = rootDirectory.GetUnixRelativePathTo(solutionFile).ToString(); parametersFile.WriteJson(dictionary); diff --git a/source/Nuke.MSBuildTasks/Nuke.MSBuildTasks.csproj b/source/Nuke.MSBuildTasks/Nuke.MSBuildTasks.csproj index 09e9ae94c..e2e4b0b6b 100644 --- a/source/Nuke.MSBuildTasks/Nuke.MSBuildTasks.csproj +++ b/source/Nuke.MSBuildTasks/Nuke.MSBuildTasks.csproj @@ -11,13 +11,13 @@ - + - - + + diff --git a/source/Nuke.ProjectModel.Tests/ProjectModelTest.cs b/source/Nuke.ProjectModel.Tests/ProjectModelTest.cs index f50dbda4a..3d77a8f89 100644 --- a/source/Nuke.ProjectModel.Tests/ProjectModelTest.cs +++ b/source/Nuke.ProjectModel.Tests/ProjectModelTest.cs @@ -29,6 +29,7 @@ public void ProjectTest() 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.7.8"); + project.GetPackageReferenceVersion("Microsoft.Build").Should().Be("17.10.4"); } [Fact] @@ -37,10 +38,10 @@ public void MSBuildProjectTest() var solution = SolutionModelTasks.ParseSolution(SolutionFile); var project = solution.Projects.Single(x => x.Name == "Nuke.ProjectModel"); - var msbuildProject = project.GetMSBuildProject(targetFramework: "net6.0"); + var msbuildProject = project.GetMSBuildProject(targetFramework: "net8.0"); - var package = msbuildProject.GetItems("PackageReference").FirstOrDefault(x => x.EvaluatedInclude == "Microsoft.Build"); + var package = msbuildProject.GetItems("PackageVersion").FirstOrDefault(x => x.EvaluatedInclude == "Microsoft.Build"); package.Should().NotBeNull(); - package.GetMetadataValue("Version").Should().Be("16.9.0"); + package.GetMetadataValue("Version").Should().Be("17.10.4"); } } diff --git a/source/Nuke.ProjectModel/Nuke.ProjectModel.csproj b/source/Nuke.ProjectModel/Nuke.ProjectModel.csproj index d3875675f..0d21f36d4 100644 --- a/source/Nuke.ProjectModel/Nuke.ProjectModel.csproj +++ b/source/Nuke.ProjectModel/Nuke.ProjectModel.csproj @@ -11,28 +11,11 @@ - - - - - - - - - - - - - - - - - - - - - - + + + + + diff --git a/source/Nuke.ProjectModel/Project.Misc.cs b/source/Nuke.ProjectModel/Project.Misc.cs index 0d15be4f4..f402fc7fe 100644 --- a/source/Nuke.ProjectModel/Project.Misc.cs +++ b/source/Nuke.ProjectModel/Project.Misc.cs @@ -58,7 +58,10 @@ public static bool HasPackageReference(this Project project, string packageId) ///
public static string GetPackageReferenceVersion(this Project project, string packageId) { - return project.GetItemMetadataSingleOrDefault("PackageReference", packageId, "Version"); + var version = project.GetItemMetadataSingleOrDefault("PackageReference", packageId, "Version"); + return version == string.Empty + ? project.GetItemMetadataSingleOrDefault("PackageVersion", packageId, "Version") + : null; } [CanBeNull] diff --git a/source/Nuke.SolutionModel/SolutionSerializer.cs b/source/Nuke.SolutionModel/SolutionSerializer.cs index 88080787d..d191a4784 100644 --- a/source/Nuke.SolutionModel/SolutionSerializer.cs +++ b/source/Nuke.SolutionModel/SolutionSerializer.cs @@ -88,6 +88,7 @@ private static Dictionary GetGlobalSection(this string[] lines, .SkipWhile(x => !Regex.IsMatch(x, $@"^\s*GlobalSection\({name}\) = \w+$")) .Skip(count: 1) .TakeWhile(x => !Regex.IsMatch(x, @"^\s*EndGlobalSection$")) + .Where(x => !x.StartsWith("#")) .ToList(); return sectionLines.Count == 0 diff --git a/source/Nuke.SourceGenerators.Tests/Nuke.SourceGenerators.Tests.csproj b/source/Nuke.SourceGenerators.Tests/Nuke.SourceGenerators.Tests.csproj index 906167b4e..65a4b6cb0 100644 --- a/source/Nuke.SourceGenerators.Tests/Nuke.SourceGenerators.Tests.csproj +++ b/source/Nuke.SourceGenerators.Tests/Nuke.SourceGenerators.Tests.csproj @@ -12,10 +12,10 @@
- - - - + + + + diff --git a/source/Nuke.SourceGenerators/Nuke.SourceGenerators.csproj b/source/Nuke.SourceGenerators/Nuke.SourceGenerators.csproj index 381f687d7..3d4e219a5 100644 --- a/source/Nuke.SourceGenerators/Nuke.SourceGenerators.csproj +++ b/source/Nuke.SourceGenerators/Nuke.SourceGenerators.csproj @@ -19,9 +19,9 @@ - - - + + + diff --git a/source/Nuke.Tooling.Generator/Nuke.Tooling.Generator.csproj b/source/Nuke.Tooling.Generator/Nuke.Tooling.Generator.csproj index 713acf1db..089faf871 100644 --- a/source/Nuke.Tooling.Generator/Nuke.Tooling.Generator.csproj +++ b/source/Nuke.Tooling.Generator/Nuke.Tooling.Generator.csproj @@ -6,11 +6,11 @@ - - - - - + + + + + diff --git a/source/Nuke.Tooling/Configure.cs b/source/Nuke.Tooling/Configure.cs index 52fc3d1f5..f3b92c135 100644 --- a/source/Nuke.Tooling/Configure.cs +++ b/source/Nuke.Tooling/Configure.cs @@ -106,10 +106,17 @@ private static IReadOnlyCollection Invoke( invocations .Where(x => x.Settings.ProcessLogOutput ?? ProcessTasks.DefaultLogOutput) .SelectMany(x => - x.Exception is not ProcessException processException - ? outputSelector(x.Result) - : processException.Process.Output) - .ForEach(x => logger(x.Type, x.Text)); + { + var (settings, result, exception) = x; + var output = exception switch + { + ProcessException processException => processException.Process.Output, + _ => outputSelector(result), + }; + + return output.Select(x => (Logger: logger ?? settings.ProcessLogger, Line: x)); + }) + .ForEach(x => x.Logger(x.Line.Type, x.Line.Text)); } } } diff --git a/source/Nuke.Tooling/NuGetPackageResolver.cs b/source/Nuke.Tooling/NuGetPackageResolver.cs index 48bd7aed7..9e40b42e3 100644 --- a/source/Nuke.Tooling/NuGetPackageResolver.cs +++ b/source/Nuke.Tooling/NuGetPackageResolver.cs @@ -211,6 +211,7 @@ public static InstalledPackage GetGlobalInstalledPackage( // packages can contain false positives due to present/missing version specification .Where(x => x.Id.EqualsOrdinalIgnoreCase(packageId)) .Where(x => !x.Version.IsPrerelease || !includePrereleases.HasValue || includePrereleases.Value) + .Distinct(x => x.Directory) .OrderByDescending(x => x.Version) .ToList(); diff --git a/source/Nuke.Tooling/Nuke.Tooling.csproj b/source/Nuke.Tooling/Nuke.Tooling.csproj index 002f32571..e6e3083d2 100644 --- a/source/Nuke.Tooling/Nuke.Tooling.csproj +++ b/source/Nuke.Tooling/Nuke.Tooling.csproj @@ -9,9 +9,9 @@ - - - + + + diff --git a/source/Nuke.Utilities.IO.Compression/Nuke.Utilities.IO.Compression.csproj b/source/Nuke.Utilities.IO.Compression/Nuke.Utilities.IO.Compression.csproj index 7cb2fc6a5..7fe82a8c6 100644 --- a/source/Nuke.Utilities.IO.Compression/Nuke.Utilities.IO.Compression.csproj +++ b/source/Nuke.Utilities.IO.Compression/Nuke.Utilities.IO.Compression.csproj @@ -9,7 +9,7 @@ - + diff --git a/source/Nuke.Utilities.IO.Globbing/Nuke.Utilities.IO.Globbing.csproj b/source/Nuke.Utilities.IO.Globbing/Nuke.Utilities.IO.Globbing.csproj index 26639f272..a43acbc68 100644 --- a/source/Nuke.Utilities.IO.Globbing/Nuke.Utilities.IO.Globbing.csproj +++ b/source/Nuke.Utilities.IO.Globbing/Nuke.Utilities.IO.Globbing.csproj @@ -9,7 +9,7 @@ - + diff --git a/source/Nuke.Utilities.Net/Nuke.Utilities.Net.csproj b/source/Nuke.Utilities.Net/Nuke.Utilities.Net.csproj index 39d63aba3..6f3aeb095 100644 --- a/source/Nuke.Utilities.Net/Nuke.Utilities.Net.csproj +++ b/source/Nuke.Utilities.Net/Nuke.Utilities.Net.csproj @@ -9,8 +9,8 @@ - - + + diff --git a/source/Nuke.Utilities.Text.Json/Nuke.Utilities.Text.Json.csproj b/source/Nuke.Utilities.Text.Json/Nuke.Utilities.Text.Json.csproj index c13a6dd63..938d355f1 100644 --- a/source/Nuke.Utilities.Text.Json/Nuke.Utilities.Text.Json.csproj +++ b/source/Nuke.Utilities.Text.Json/Nuke.Utilities.Text.Json.csproj @@ -9,7 +9,7 @@ - + diff --git a/source/Nuke.Utilities.Text.Yaml/Nuke.Utilities.Text.Yaml.csproj b/source/Nuke.Utilities.Text.Yaml/Nuke.Utilities.Text.Yaml.csproj index 1cd42cb3d..aefd1f0b9 100644 --- a/source/Nuke.Utilities.Text.Yaml/Nuke.Utilities.Text.Yaml.csproj +++ b/source/Nuke.Utilities.Text.Yaml/Nuke.Utilities.Text.Yaml.csproj @@ -9,7 +9,7 @@ - + diff --git a/source/Nuke.Utilities/Nuke.Utilities.csproj b/source/Nuke.Utilities/Nuke.Utilities.csproj index 1b9bf611e..c4ed59fc1 100644 --- a/source/Nuke.Utilities/Nuke.Utilities.csproj +++ b/source/Nuke.Utilities/Nuke.Utilities.csproj @@ -5,7 +5,7 @@ - +