cake-build / cake

:cake: Cake (C# Make) is a cross platform build automation system.
https://cakebuild.net
MIT License
3.89k stars 726 forks source link

When running multiple targets, common dependent tasks are executed twice #4324

Open CharliePoole opened 1 month ago

CharliePoole commented 1 month ago

UPDATED to provide a simple repro

Prerequisites

Cake runner

Cake .NET Tool

Cake version

4.0.0

Operating system

Windows

Operating system architecture

64-Bit

What are you seeing?

When running tests for the NUnit console runner with arguments --target=Test --target=Package the Build target is run twice, once before Test and once before Package. Build is, of course, a dependency of both targets.

I would expect the dependent target to run only once

NOTE: This appears to be related to #4066.

Steps to Reproduce

Use the following script:

var targets = Arguments<string>("target", "A");

Task("A")
    .Does(() => Information("Running A"));

Task("B")
    .IsDependentOn("A")
    .Does(() => Information("Running B"));

Task("C")
    .IsDependentOn("A")
    .Does(() => Information("Running C"));

RunTargets(targets);

Run using arguments --target=B --target=C

This produces the following output:

========================================
A
========================================
Running A

========================================
B
========================================
Running B

========================================
A
========================================
Running A

========================================
C
========================================
Running C

Task                          Duration
--------------------------------------------------
A                             00:00:00.0060125
B                             00:00:00.0003280
A                             00:00:00.0001377
C                             00:00:00.0001879
--------------------------------------------------
Total:                        00:00:00.0066661
CharliePoole commented 1 month ago

@devlead I modified a version of the above to use similar code to what you proposed in 2022 as a workaround. That code, as expected, runs A B and C in sequence.

Here's the updated script:

var targets = Arguments<string>("target", "A");

Task("A")
    .Does(() => Information("Running A"));

Task("B")
    .IsDependentOn("A")
    .Does(() => Information("Running B"));

Task("C")
    .IsDependentOn("A")
    .Does(() => Information("Running C"));

RunTargets(targets); // This fails, running A twice
//MyRunTargets(targets); // This succeeds, running A only once

CakeReport MyRunTargets(ICollection<string> targets)
    => RunTarget(GetOrAddTargetsTask(targets).Name);

Task<CakeReport> RunTargetsAsync(ICollection<string> targets)
    => RunTargetAsync(GetOrAddTargetsTask(targets).Name);

private ICakeTaskInfo GetOrAddTargetsTask(ICollection<string> targets)
{
    var targetsTaskName = string.Join(',', targets);
    var targetsTask = Tasks.FirstOrDefault(
        task => task.Name == targetsTaskName
    );
    if (targetsTask == null)
    {
        var task = Task(targetsTaskName);
        foreach(var target in targets)
        {
            task.IsDependentOn(target);
        }
        targetsTask = task.Task;
    }
    return targetsTask;
}