Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Full interactive mode (ask for target) #1436

Open
Danielku15 opened this issue Oct 23, 2024 · 5 comments
Open

Full interactive mode (ask for target) #1436

Danielku15 opened this issue Oct 23, 2024 · 5 comments

Comments

@Danielku15
Copy link
Contributor

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:

​
███╗   ██╗██╗   ██╗██╗  ██╗███████╗
████╗  ██║██║   ██║██║ ██╔╝██╔════╝
██╔██╗ ██║██║   ██║█████╔╝ █████╗  
██║╚██╗██║██║   ██║██╔═██╗ ██╔══╝  
██║ ╚████║╚██████╔╝██║  ██╗███████╗
╚═╝  ╚═══╝ ╚═════╝ ╚═╝  ╚═╝╚══════╝
​
NUKE Execution Engine version 8.1.2 (Windows,.NETCoreApp,Version=v8.0)
​
Targets (with their direct dependencies):

  > PrepareEnviroment
      Checks for any missing environment configurations on your machine and sets up whatever is needed.
    Build
      Compile the whole project.
    Test
      Run the tests of this project 

Select which target you want to run with the arrow keys and run it with [ENTER].

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.

​
███╗   ██╗██╗   ██╗██╗  ██╗███████╗
████╗  ██║██║   ██║██║ ██╔╝██╔════╝
██╔██╗ ██║██║   ██║█████╔╝ █████╗  
██║╚██╗██║██║   ██║██╔═██╗ ██╔══╝  
██║ ╚████║╚██████╔╝██║  ██╗███████╗
╚═╝  ╚═══╝ ╚═════╝ ╚═╝  ╚═╝╚══════╝
​
NUKE Execution Engine version 8.1.2 (Windows,.NETCoreApp,Version=v8.0)
​
Targets (with their direct dependencies):

  PrepareEnviroment
    Checks for any missing environment configurations on your machine and sets up whatever is needed.
  Build
    Compile the whole project.
  Test
    Run the tests of this project 

Which target do you want to run (default: PrepareEnvironment): _

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

@Danielku15
Copy link
Contributor Author

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.

@matkoch
Copy link
Member

matkoch commented Oct 23, 2024

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.

@Danielku15
Copy link
Contributor Author

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:

  • We directly call a method similar to protected internal virtual IReadOnlyCollection<ExecutableTarget>? OnSelectTargetsInteractively(IReadOnlyCollection<ExecutableTarget> executableTargets) on NukeBuild.
  • NukeBuild ships a default implementation for requesting a target interactively.
    • Unless we want to make interactive targets a default, a separate flag would need to be checked whether to ask at all.
    • If the interactive mode is enabled, NukeBuild calls over to Host, e.g. Host.OnSelectTargetsInteractively().
    • The Terminal implementation we print the list to the stdout, and reads from stdin which target to execute.
    • CI Host implementations would not do anything. I am not aware if any CI system has features to ask for inputs dynamically. Checking for 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;
}

@matkoch
Copy link
Member

matkoch commented Oct 23, 2024

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.

@Danielku15
Copy link
Contributor Author

@matkoch I started with a proposal here #1437 let me know what you think 😁

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

2 participants