Cake: Provide access to the run target and ordered list of tasks

Created on 30 Aug 2017  路  10Comments  路  Source: cake-build/cake

Motivation

I had a use case today that required the capability to determine in Setup whether a certain target would be executed.

This is less useful for a simple script where you can see everything right in front of you. For utility code as part of a larger script or for a module, this is crucial for the reasons listed below.

The journey ended with a need for proper access inside Setup to the list of tasks about to be run. Modules are no help because they share the same limitations and even run into a few more than the script access.

No access to the run target

Currently there is no way for a script or module to know what the run target is without resorting to using reflection to mutate the readonly compiler-generated backing fields of the host context, swapping out the ICakeEngine instance with a delegating interceptor to capture the target parameter on the RunTarget method. A module's only alternative to reflection hacks is would be to replace the cake engine without knowing what the original implementation was (https://github.com/cake-build/cake/issues/1770) which results in compatibility issues with other modules.

It's _not_ acceptable to simply assume that the argument passed to RunTarget was precisely Argument("target", "Default"). People may have other defaults, conditionals, or odd use cases. We need to know, in the context of the run that Setup is called for, deterministically, what the target was.

No access to the final resolved dependency path

CakeEngine uses the internal CakeGraphBuilder and CakeGraph to obtain a sequence of CakeTasks but never exposes them. The calculation is even at the perfect place to hand them to the Setup handler.
Scripts and modules are forced to use reflection and waste cycles on duplicating the exact calculation that CakeEngine already does.

Future extensibility

With these things in place a module would be able to implement things such as a task.BeforeDependencies(() => ... handler, as @devlead suggested.

Usage

```c#
Setup(context =>
{
if (context.TasksToExecute.Any(task => task == allTestsTask))
{
// ...
}

if (context.TasksToExecute.Last().Name == "Foo")
{
    // ...
}

});

## Implementation

The change to CakeEngine is so small it's almost begging to be done: https://github.com/cake-build/cake/commit/5c7da371611863f96909dd3ab7c14a5dc962566d#diff-288fcd3a724914ae341fe456d42ca525R132

In order to expose this via `Setup(context => context.TasksToExecute`, we'll need to make an `ISetupContext` to extend the `ICakeContext` which `Setup` currently exposes. This means we'll need to make a breaking change to `IExecutionStrategy`, just like you did with teardown in https://github.com/cake-build/cake/issues/1089:

```diff
-void PerformSetup(Action<ICakeContext> action, ICakeContext context);
+void PerformSetup(Action<ISetupContext> action, ISetupContext context);

It's better to do this sooner rather than later. It allows any extension in the future to be non-breaking.

Every change made brings setup into symmetry with teardown: https://github.com/cake-build/cake/commit/5c7da371611863f96909dd3ab7c14a5dc962566d

Finally, it also seems worthwhile to consider returning the CakeTasks rather than just the strings since CakeEngine is doing this anyway. It's more user-friendly; the alternative forces people to first look up the right task via Tasks.Single(task => task.Name.Equals(x, StringComparison.OrdinalIgnoreCase)).

This commit changes IReadOnlyList<string> to IReadOnlyList<CakeTask>: https://github.com/cake-build/cake/commit/5fd3cc6aa37a7af5d617b9ab262b73c46fdb02f6

Feature

Most helpful comment

This functionality is included in #2008 which hopefully will be available in 0.28.0.

All 10 comments

I wanted this yet again yesterday:

var testBinDirPattern = $"src/**/*.Tests/bin/{configuration}";

Task("Clean")
    .IsDependentOn("Restore")
    .Does(() =>
    {
        if (context.TasksToExecute.Any(task => task.Name == "Test"))
        {
            // Needed in case target frameworks change and leave stale artifacts that MSBuild doesn't clean
            CleanDirectories(testBinDirPattern);
        }

        MSBuild("src", settings => settings.SetConfiguration(configuration).WithTarget("Clean"));
    });

Task("Build")
    .IsDependentOn("Clean")
    .Does(() =>
    {
        MSBuild("src", settings => settings.SetConfiguration(configuration).WithTarget("Build"));
    });

Task("Test")
    .IsDependentOn("Build")
    .Does(() =>
    {
        NUnit3(testBinDirPattern + "/**/*.Tests.dll");
    });

Task("Pack")
    .IsDependentOn("Test")
    .Does(() =>
    {
        MSBuild("src/ProjectToPack", settings => settings.SetConfiguration(configuration).WithTarget("Pack"));
    });

Of course, I could just always do the extra cleaning in case tests are going to be run so this isn't the best example.

I just hit this use case today as well with the use of VSTS. Currently, using the VSTS Cake task a single Cake task is being reported. Ideally, I'd during Setup I'd like to tell VSTS the individual Cake tasks that will execute then report their progress using TFBuildCommands.

image

This functionality is included in #2008 which hopefully will be available in 0.28.0.

@patriksvensson That's great news! I'm not laying eyes on it in https://github.com/cake-build/cake/issues/2008. What will a user's code look like which looks ahead at the list of tasks that will execute?

@jnm2 The Setup-step now provides a custom context (ISetupContext) that has a TasksToExecute property.

    public interface ISetupContext : ICakeContext
    {
        /// <summary>
        /// Gets all registered tasks that are going to be executed.
        /// </summary>
        IReadOnlyCollection<ICakeTaskInfo> TasksToExecute { get; }
    }

Thank you, this is great! I can hardly wait!

This is all very nice! I'm wondering about the best way to get access to the TasksToExecute from inside a task. Is this the simplest approach:

IReadOnlyCollection<ICakeTaskInfo> tasksToExecute;

Setup(context =>
{
    tasksToExecute = context.TasksToExecute;
});

Task("X")
{
    if (context.TasksToExecute.Any(task => task.Name == "Test"))
    {
    ...
    }
}

@mdesousa Take a look at the new typed context data. This might be a good solution for your problem.

Sounds good! Is there a link to documentation or api? I'm not familiar with it...

Ah, right there in the release notes for 0.28.0: https://cakebuild.net/blog/2018/05/cake-v0.28.0-released
Thanks, I'll take a look.

Was this page helpful?
0 / 5 - 0 ratings