Skip to content
This repository was archived by the owner on May 15, 2025. It is now read-only.

feat: Add Interactive mode with target selection #1437

Draft
wants to merge 5 commits into
base: develop
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
107 changes: 107 additions & 0 deletions build/AwtrixNotificationsAttribute.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
// Copyright 2024 Maintainers of NUKE.
// Distributed under the MIT License.
// https://github.com/nuke-build/nuke/blob/master/LICENSE

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using JetBrains.Annotations;
using MQTTnet;
using MQTTnet.Client;
using MQTTnet.Protocol;
using Newtonsoft.Json;
using Nuke.Common;
using Nuke.Common.Execution;

[PublicAPI]
public class AwtrixNotificationsAttribute : BuildExtensionAttributeBase,
IOnBuildCreated,
IOnTargetRunning,
IOnBuildFinished
{
private readonly IMqttClient _mqttClient;

public string MqttHost => EnvironmentInfo.GetVariable("NUKE_AWTRIX_MQTT_HOST");
public int? MqttPort => EnvironmentInfo.GetVariable<int?>("NUKE_AWTRIX_MQTT_PORT").NotNull();
public string MqttTopicPrefix => EnvironmentInfo.GetVariable("NUKE_AWTRIX_MQTT_PREFIX").NotNullOrWhiteSpace();

public AwtrixNotificationsAttribute()
{
if (MqttHost == null)
return;

_mqttClient = new MqttFactory().CreateMqttClient();
_mqttClient.ConnectAsync(new MqttClientOptionsBuilder()
.WithTcpServer(MqttHost, MqttPort)
.Build(),
CancellationToken.None).GetAwaiter().GetResult();
}

public int RunningIconId { get; set; } = 13546;
public int SuccessIconId { get; set; } = 39396;
public int FailureIconId { get; set; } = 8510;
public string ProjectName { get; set; } = "Build";

protected virtual object CreateBuildCreatedPayload()
{
return new
{
icon = RunningIconId,
text = ProjectName,
hold = true,
stack = false,
};
}

protected virtual object CreateTargetRunningPayload(ExecutableTarget target)
{
var progress = (Build.SucceededTargets.Count + Build.FailedTargets.Count) / (double)Build.ExecutionPlan.Count * 100;
return new
{
icon = RunningIconId,
text = target.Name,
progress,
progressC = Build.IsSucceeding ? "#00ff00" : "#ff0000",
progressBC = "#333333",
hold = true,
stack = false,
};
}

protected virtual object CreateBuildFinishedPayload()
{
return new
{
icon = Build.IsSucceeding ? SuccessIconId : FailureIconId,
text = ProjectName,
hold = !Build.IsSucceeding,
stack = false,
};
}

public void OnBuildCreated(IReadOnlyCollection<ExecutableTarget> executableTargets)
{
PublishMqttMessage(CreateBuildCreatedPayload());
}

public void OnTargetRunning(ExecutableTarget target)
{
PublishMqttMessage(CreateTargetRunningPayload(target));
}

public void OnBuildFinished()
{
PublishMqttMessage(CreateBuildFinishedPayload());
}

private void PublishMqttMessage(object payload)
{
_mqttClient?.PublishAsync(new MqttApplicationMessageBuilder()
.WithTopic($"{MqttTopicPrefix}/notify")
.WithPayload(JsonConvert.SerializeObject(payload))
.WithQualityOfServiceLevel(MqttQualityOfServiceLevel.ExactlyOnce)
.Build(),
CancellationToken.None).GetAwaiter().GetResult();
}
}
1 change: 1 addition & 0 deletions build/_build.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@

<ItemGroup>
<PackageReference Include="linqtotwitter" Version="6.15.0" />
<PackageReference Include="MQTTnet" Version="4.3.7.1207" />
<PackageReference Include="SixLabors.ImageSharp.Drawing" Version="2.0.1" />
<PackageDownload Include="Codecov.Tool" Version="[1.13.0]" />
<PackageDownload Include="GitVersion.Tool" Version="[5.12.0]" />
Expand Down
4 changes: 1 addition & 3 deletions source/Nuke.Build.Tests/ExecutionPlannerTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -96,9 +96,7 @@ public void TestExternalTrigger()

private IEnumerable<ExecutableTarget> GetPlan(ExecutableTarget[] invokedTargets = null)
{
static string[] SelectNames(ExecutableTarget[] targets) => targets?.Select(x => x.Name).ToArray();

return ExecutionPlanner.GetExecutionPlan(new[] { A, B, C }, SelectNames(invokedTargets));
return ExecutionPlanner.GetExecutionPlan(new[] { A, B, C }, invokedTargets);
}

private void AddTrigger(ExecutableTarget source, ExecutableTarget target)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,10 @@
"description": "Host for execution. Default is 'automatic'",
"$ref": "#/definitions/Host"
},
"Interactive": {
"type": "boolean",
"description": "Activates the interactive mode when asking for user inputs"
},
"NoLogo": {
"type": "boolean",
"description": "Disables displaying the NUKE logo"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@
"description": "Host for execution. Default is 'automatic'",
"$ref": "#/definitions/Host"
},
"Interactive": {
"type": "boolean",
"description": "Activates the interactive mode when asking for user inputs"
},
"NoLogo": {
"type": "boolean",
"description": "Disables displaying the NUKE logo"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,10 @@
"description": "Host for execution. Default is 'automatic'",
"$ref": "#/definitions/Host"
},
"Interactive": {
"type": "boolean",
"description": "Activates the interactive mode when asking for user inputs"
},
"NoLogo": {
"type": "boolean",
"description": "Disables displaying the NUKE logo"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,10 @@
"description": "Host for execution. Default is 'automatic'",
"$ref": "#/definitions/Host"
},
"Interactive": {
"type": "boolean",
"description": "Activates the interactive mode when asking for user inputs"
},
"NoLogo": {
"type": "boolean",
"description": "Disables displaying the NUKE logo"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ protected IEnumerable<ExecutableTarget> GetInvokedTargets(ExecutableTarget execu

Assert.True(invokedTargets.Except(new[] { executableTarget }).Count(x => x.PartitionSize != null) == 0,
$"Non-entry targets for {executableTarget.Name} cannot define partitions");
return ExecutionPlanner.GetExecutionPlan(invokedTargets, new[] { executableTarget.Name });
return ExecutionPlanner.GetExecutionPlan(invokedTargets, new[] { executableTarget });
}

protected IEnumerable<ExecutableTarget> GetTargetDependencies(ExecutableTarget executableTarget)
Expand Down
2 changes: 1 addition & 1 deletion source/Nuke.Build/CICD/ConfigurationAttributeBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ public abstract class ConfigurationAttributeBase : Attribute, IConfigurationGene
public void Generate(IReadOnlyCollection<ExecutableTarget> executableTargets)
{
var relevantTargets = RelevantTargetNames
.SelectMany(x => ExecutionPlanner.GetExecutionPlan(executableTargets, new[] { x }))
.SelectMany(x => ExecutionPlanner.GetExecutionPlan(executableTargets, new[] { ExecutableTargetFactory.GetExecutableTarget(x, executableTargets) }))
.Distinct()
.Where(x => !IrrelevantTargetNames.Contains(x.Name)).ToList();
var configuration = GetConfiguration(relevantTargets);
Expand Down
6 changes: 3 additions & 3 deletions source/Nuke.Build/Execution/BuildManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -62,10 +62,10 @@ public static int Execute<T>(Expression<Func<T, Target>>[] defaultTargetExpressi
if (!build.NoLogo)
build.WriteLogo();

// TODO: move InvokedTargets to ExecutableTargetFactory
var invokedTargets = ExecutableTargetFactory.CollectInvokedTargets(build);
build.ExecutionPlan = ExecutionPlanner.GetExecutionPlan(
build.ExecutableTargets,
ParameterService.GetParameter<string[]>(() => build.InvokedTargets));
invokedTargets);

ToolRequirementService.EnsureToolRequirements(build, build.ExecutionPlan);
build.ExecuteExtension<IOnBuildInitialized>(x => x.OnBuildInitialized(build.ExecutableTargets, build.ExecutionPlan));
Expand All @@ -75,7 +75,7 @@ public static int Execute<T>(Expression<Func<T, Target>>[] defaultTargetExpressi
build,
ParameterService.GetParameter<string[]>(() => build.SkippedTargets));

return build.ExitCode ??= build.IsSuccessful ? 0 : ErrorExitCode;
return build.ExitCode ??= build.IsSucceeding ? 0 : ErrorExitCode;
}
catch (Exception exception)
{
Expand Down
34 changes: 33 additions & 1 deletion source/Nuke.Build/Execution/ExecutableTargetFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using JetBrains.Annotations;
using Nuke.Common.Utilities;
using Nuke.Common.Utilities.Collections;

Expand All @@ -17,6 +18,37 @@ namespace Nuke.Common.Execution;
/// </summary>
internal static class ExecutableTargetFactory
{
[CanBeNull]
public static IReadOnlyCollection<ExecutableTarget> CollectInvokedTargets<T>(T build)
where T : INukeBuild
{
var buildType = build.GetType();
var invokedTargetProperty = buildType.GetProperty(nameof(INukeBuild.InvokedTargets));
var invokedTargetNames = ParameterService.GetParameter<string[]>(invokedTargetProperty);
if (invokedTargetNames is { Length: > 0 })
{
return invokedTargetNames.Select(x => GetExecutableTarget(x, build.ExecutableTargets)).ToList();
}

return build.OnNoTargetsSpecified();
}

internal static ExecutableTarget GetExecutableTarget(
string targetName,
IReadOnlyCollection<ExecutableTarget> executableTargets)
{
targetName = targetName.Replace("-", string.Empty);
var executableTarget = executableTargets.SingleOrDefault(x => x.Name.EqualsOrdinalIgnoreCase(targetName));
if (executableTarget == null)
{
Assert.Fail($"Target with name {targetName.SingleQuote()} does not exist. Available targets are:"
.Concat(executableTargets.Select(x => $" - {x.Name}").OrderBy(x => x))
.JoinNewLine());
}

return executableTarget;
}

public static IReadOnlyCollection<ExecutableTarget> CreateAll<T>(
T build,
params Expression<Func<T, Target>>[] defaultTargetExpressions)
Expand All @@ -35,7 +67,7 @@ ExecutableTarget Create(PropertyInfo property)
.Reverse().ToList();
var definition = new TargetDefinition(property, build, new Stack<PropertyInfo>(baseMembers));

var factory = (Delegate) property.GetValue(build);
var factory = (Delegate)property.GetValue(build);
factory.DynamicInvokeUnwrap(definition);

return new ExecutableTarget
Expand Down
21 changes: 2 additions & 19 deletions source/Nuke.Build/Execution/ExecutionPlanner.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,8 @@ internal static class ExecutionPlanner
{
public static IReadOnlyCollection<ExecutableTarget> GetExecutionPlan(
IReadOnlyCollection<ExecutableTarget> executableTargets,
[CanBeNull] IReadOnlyCollection<string> invokedTargetNames)
[CanBeNull] IReadOnlyCollection<ExecutableTarget> invokedTargets)
{
var invokedTargets = invokedTargetNames?.Select(x => GetExecutableTarget(x, executableTargets)).ToList();
invokedTargets?.ForEach(x => x.Invoked = true);

// Repeat to create the plan with triggers taken into account until plan doesn't change
Expand All @@ -42,7 +41,7 @@ public static IReadOnlyCollection<ExecutableTarget> GetExecutionPlan(

private static IReadOnlyCollection<ExecutableTarget> GetExecutionPlanInternal(
IReadOnlyCollection<ExecutableTarget> executableTargets,
ICollection<ExecutableTarget> invokedTargets)
IReadOnlyCollection<ExecutableTarget> invokedTargets)
{
var vertexDictionary = GetVertexDictionary(executableTargets);
var graphAsList = vertexDictionary.Values.ToList();
Expand Down Expand Up @@ -95,20 +94,4 @@ private static IReadOnlyDictionary<ExecutableTarget, Vertex<ExecutableTarget>> G

return vertexDictionary;
}

private static ExecutableTarget GetExecutableTarget(
string targetName,
IReadOnlyCollection<ExecutableTarget> executableTargets)
{
targetName = targetName.Replace("-", string.Empty);
var executableTarget = executableTargets.SingleOrDefault(x => x.Name.EqualsOrdinalIgnoreCase(targetName));
if (executableTarget == null)
{
Assert.Fail($"Target with name {targetName.SingleQuote()} does not exist. Available targets are:"
.Concat(executableTargets.Select(x => $" - {x.Name}").OrderBy(x => x))
.JoinNewLine());
}

return executableTarget;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -36,18 +36,31 @@ public string GetTargetsText()
builder.AppendLine();
foreach (var target in Build.ExecutableTargets.Where(x => x.Listed))
{
var dependencies = target.ExecutionDependencies.Count > 0
? $" -> {target.ExecutionDependencies.Select(x => x.Name).JoinCommaSpace()}"
: string.Empty;
var targetEntry = target.Name + (target.IsDefault ? " (default)" : string.Empty);
builder.AppendLine($" {targetEntry.PadRight(padRightTargets)}{dependencies}");
if (!string.IsNullOrWhiteSpace(target.Description))
builder.AppendLine($" {target.Description}");
AppendTargetText(builder, target,
" ",
" ",
padRightTargets);
}

return builder.ToString();
}

internal static void AppendTargetText(StringBuilder builder, ExecutableTarget target,
string targetIndent,
string descriptionIndent,
int padRightTargets)
{
var dependencies = target.ExecutionDependencies.Count > 0
? $" -> {target.ExecutionDependencies.Select(x => x.Name).JoinCommaSpace()}"
: string.Empty;
var targetEntry = target.Name + (target.IsDefault ? " (default)" : string.Empty);
builder.AppendLine($"{targetIndent}{targetEntry.PadRight(padRightTargets)}{dependencies}");
if (!string.IsNullOrWhiteSpace(target.Description))
{
builder.AppendLine($"{descriptionIndent}{target.Description}");
}
}

public string GetParametersText()
{
var defaultTargets = Build.ExecutableTargets.Where(x => x.IsDefault).Select(x => x.Name).ToList();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,13 +84,13 @@ private string GetEvents()

// When not hovering anything, highlight the default plan
var defaultPlan = executableTargets.Where(x => x.IsDefault)
.SelectMany(x => ExecutionPlanner.GetExecutionPlan(executableTargets, new[] { x.Name }))
.SelectMany(x => ExecutionPlanner.GetExecutionPlan(executableTargets, new[] { x }))
.Distinct().ToList();
defaultPlan.ForEach(x => builder.AppendLine($@" $(""#{x.Name}"").addClass('highlight');"));

foreach (var executableTarget in executableTargets)
{
var executionPlan = ExecutionPlanner.GetExecutionPlan(executableTargets, new[] { executableTarget.Name });
var executionPlan = ExecutionPlanner.GetExecutionPlan(executableTargets, new[] { executableTarget });
builder
.AppendLine($@" $(""#{executableTarget.Name}"").hover(")
.AppendLine(" function() {");
Expand Down
2 changes: 1 addition & 1 deletion source/Nuke.Build/Host.cs
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,7 @@ static string GetInformation(ExecutableTarget target)
protected internal virtual void WriteBuildOutcome(INukeBuild build)
{
Debug();
if (build.IsSuccessful)
if (build.IsSucceeding)
Success($"Build succeeded on {DateTime.Now.ToString(CultureInfo.CurrentCulture)}. \(^ᴗ^)/");
else
Error($"Build failed on {DateTime.Now.ToString(CultureInfo.CurrentCulture)}. (╯°□°)╯︵ ┻━┻");
Expand Down
Loading
Loading