Open Danielku15 opened 1 month ago
Note: I digged up some old code which was referenced on some other issues and tried to implement a custom interactive mode similar. But the build log seems to be locked by the current process when I launch the second build.
public class InteractiveBuildAttribute : BuildExtensionAttributeBase, IOnBuildInitialized
{
public void OnBuildInitialized(
IReadOnlyCollection<ExecutableTarget> executableTargets,
IReadOnlyCollection<ExecutableTarget> executionPlan)
{
if (!Build.Help && executionPlan.Count == 0 && NukeBuild.Host is Terminal)
{
Log.Information("No target specified, please select the target you want to run:");
foreach (var target in executableTargets.Where(x => x.Listed))
{
Log.Information("{TargetName}", target.Name);
if (!string.IsNullOrWhiteSpace(target.Description))
{
Log.Information(" {Description}", target.Description);
}
}
var defaultTarget = nameof(MySharedNukeBuild.Compile);
Log.Information(
"Type the name of the target you want to execute and hit [ENTER] (default: {DefaultTarget})",
defaultTarget);
Console.Write("> ");
var selectedTarget = Console.ReadLine();
if (string.IsNullOrEmpty(selectedTarget))
{
selectedTarget = defaultTarget;
}
else if (!executableTargets.Any(t => selectedTarget.EqualsOrdinalIgnoreCase(t.Name)))
{
Log.Error("Unknown target '{SelectedTarget}' specified, exiting", selectedTarget);
Environment.Exit(1);
}
Log.Information("Starting target {SelectedTarget}", selectedTarget);
var assembly = Assembly.GetEntryAssembly()!.Location;
var exitCode = 0;
DotNetTasks.DotNet($"{assembly} -- --nologo --target {selectedTarget}",
logInvocation: false,
exitHandler: p => exitCode = p.ExitCode);
Environment.Exit(exitCode);
}
}
}
// HandleHelpRequests has Priority 5, we want to be before it
[InteractiveBuild(Priority = 7)]
public class MySharedNukeBuild;
17:20:04 [DBG] The process cannot access the file 'D:\my-project\.nuke\temp\build.log' because it is being used by another process.
Even though it would still be great to have this built-in I'd be fine with implementing it myself via BuildExtensionAttributeBase
. Any hints in how to avoid resource locks with multiple Nuke processes would be awesome.
I would not implement it that way. I would rather extend BuildManager
to become interactive if there's no default target, or if --interactive
(new parameter) was passed. Then it could continue to operate in the same process, effectively providing what is usually passed in the Main
method.
Assuming we'd extend things here. How would you prefer to trigger the interactive flow? If we can agree on some path that also makes you happy, I'd could work on a PR 😉 Here some thoughts and proposals:
One idea could be an extension which allows handling this scenario in custom ways and devs implement the logic as they like:
var invokedTargets = ParameterService.GetParameter<string[]>(() => build.InvokedTargets);
if (build.IsInteractive || invokedTargets is not { Length: > 0 })
{
var newTargetList = new List<ExecutableTarget>();
build.ExecuteExtension<IOnBuildInteractiveTargets>(x => x.OnSelectTargetsInteractively(build.ExecutableTargets, newTargetList));
if (newTargetList.Count > 0)
{
invokedTargets = newTargetList.Select(t => t.Name).ToArray();
}
}
build.ExecutionPlan = ExecutionPlanner.GetExecutionPlan(
build.ExecutableTargets,
invokedTargets );
The alternative would be to go for a more direct call path to NukeBuild
and Host
:
protected internal virtual IReadOnlyCollection<ExecutableTarget>? OnSelectTargetsInteractively(IReadOnlyCollection<ExecutableTarget> executableTargets)
on NukeBuild
. NukeBuild
ships a default implementation for requesting a target interactively.
NukeBuild
calls over to Host, e.g. Host.OnSelectTargetsInteractively()
. Terminal
implementation we print the list to the stdout
, and reads from stdin
which target to execute.Host is Terminal
in NukeBuild.OnSelectTargetsInteractively
feels wrong but is also an option. In code similar to:
// BuildManager.cs
var invokedTargets = ParameterService.GetParameter<string[]>(() => build.InvokedTargets);
if (invokedTargets is not { Length: > 0 })
{
invokedTargets = build.OnSelectTargetsInteractively(build.ExecutableTargets)?.Select(t => t.Name).ToArray();
}
build.ExecutionPlan = ExecutionPlanner.GetExecutionPlan(
build.ExecutableTargets,
invokedTargets);
// NukeBuild.cs
protected bool SelectTargetsInteractively { get; set; } // opt-in
protected virtual IReadOnlyCollection<ExecutableTarget>? OnSelectTargetsInteractively(IReadOnlyCollection<ExecutableTarget> executableTargets)
{
if (IsInteractive || SelectTargetsInteractively)
{
return Host.OnSelectTargetsInteractively(executableTargets);
}
else
{
return null;
}
}
// Host.cs
protected virtual IReadOnlyCollection<ExecutableTarget>? OnSelectTargetsInteractively(IReadOnlyCollection<ExecutableTarget> executableTargets)
{
return null;
}
// Terminal.cs
protected override IReadOnlyCollection<ExecutableTarget>? OnSelectTargetsInteractively(IReadOnlyCollection<ExecutableTarget> executableTargets)
{
PrintPrompt(executableTargets);
var userInput = Console.ReadLine().Split(',');
var target = userInput.Select(i => executableTargets.FirstOrDefault(t => i.EqualsOrdinalIgnoreCase(t.Name)).Where(t => t != null).ToReadOnlyCollection();
if (target.Count != userInput.Length)
{
PrintError(userInput);
return null;
}
return target;
}
In the first iteration, I would keep it simpler and implement it without extension points. Without much research, I think ExecutableTargetFactory
is a good place to do this. I already left a comment there, that ParameterService.GetParameter<string[]>(() => build.InvokedTargets)
should be handled in that class. I also wouldn't mind adding a reference to SpectreConsole
in Nuke.Build
(there's a fork referenced in Nuke.GlobalTool
already) to allow multi-selection.
@matkoch I started with a proposal here https://github.com/nuke-build/nuke/pull/1437 let me know what you think 😁
Description
Nuke is great and very flexible when it comes to CLI usage and is very flexible in the usage from CI/CD systems. What I am missing a bit, is a modern user experience when developers use the "_build" project locally.
If the project/build is started without any input, and we detect a "Terminal" host, we enter an interactive mode to ask the user what to do. e.g. we have 5 different entry points to our build depending on what you want to do.
Usage Example
When I launch the "_build.exe" (or dotnet run) Nuke should prompt the user:
Then it starts the respective target just as if
nuke --target PrepareEnvironment
is called. The way of asking could be either fully interactive like very modern console apps, or simply asking for the user to type it.Alternative
I thought of making some unlisted target "interactive" the default one. Then I do any console interactivity myself (by loading the targets and stuff myself).
After the user has chosen the target, I would launch the target.
a) I cannot directly trigger another target within the same process. But it would be nice if I can have a dynamic "Triggers" in this target. b) I trigger the target via
DotNetTasks.DotNetRun(<current assembly, target and parameters>)
Could you help with a pull-request?
Yes