diff --git a/setup.go b/setup.go index c690cbe9..b3de3394 100644 --- a/setup.go +++ b/setup.go @@ -79,6 +79,9 @@ func (e *Executor) readTaskfile(node taskfile.Node) error { if err := graph.Visualize("./taskfile-dag.gv"); err != nil { return err } + if e.Taskfile, err = graph.Merge(); err != nil { + return err + } return nil } diff --git a/taskfile/ast/graph.go b/taskfile/ast/graph.go index 3e30faa6..7d69d8cc 100644 --- a/taskfile/ast/graph.go +++ b/taskfile/ast/graph.go @@ -1,10 +1,14 @@ package ast import ( + "fmt" "os" "github.com/dominikbraun/graph" "github.com/dominikbraun/graph/draw" + "golang.org/x/sync/errgroup" + + "github.com/go-task/task/v3/internal/filepathext" ) type TaskfileGraph struct { @@ -31,11 +35,118 @@ func NewTaskfileGraph() *TaskfileGraph { } } -func (r *TaskfileGraph) Visualize(filename string) error { +func (tfg *TaskfileGraph) Visualize(filename string) error { f, err := os.Create(filename) if err != nil { return err } defer f.Close() - return draw.DOT(r.Graph, f) + return draw.DOT(tfg.Graph, f) +} + +func (tfg *TaskfileGraph) Merge() (*Taskfile, error) { + hashes, err := graph.TopologicalSort(tfg.Graph) + if err != nil { + return nil, err + } + + predecessorMap, err := tfg.PredecessorMap() + if err != nil { + return nil, err + } + + for i := len(hashes) - 1; i >= 0; i-- { + hash := hashes[i] + + // Get the current vertex + vertex, err := tfg.Vertex(hash) + if err != nil { + return nil, err + } + + // Create an error group to wait for all the included Taskfiles to be merged with all its parents + var g errgroup.Group + + // Loop over each adjacent edge + for _, edge := range predecessorMap[hash] { + + // TODO: Enable goroutines + // Start a goroutine to process each included Taskfile + // g.Go( + err := func() error { + // Get the child vertex + predecessorVertex, err := tfg.Vertex(edge.Source) + if err != nil { + return err + } + + // Get the merge options + include, ok := edge.Properties.Data.(*Include) + if !ok { + return fmt.Errorf("task: Failed to get merge options") + } + + // Handle advanced imports + // i.e. where additional data is given when a Taskfile is included + if include.AdvancedImport { + predecessorVertex.Taskfile.Vars.Range(func(k string, v Var) error { + o := v + o.Dir = include.Dir + predecessorVertex.Taskfile.Vars.Set(k, o) + return nil + }) + predecessorVertex.Taskfile.Env.Range(func(k string, v Var) error { + o := v + o.Dir = include.Dir + predecessorVertex.Taskfile.Env.Set(k, o) + return nil + }) + for _, task := range vertex.Taskfile.Tasks.Values() { + task.Dir = filepathext.SmartJoin(include.Dir, task.Dir) + if task.IncludeVars == nil { + task.IncludeVars = &Vars{} + } + task.IncludeVars.Merge(include.Vars) + task.IncludedTaskfileVars = vertex.Taskfile.Vars + } + } + + // Merge the included Taskfile into the parent Taskfile + if err := predecessorVertex.Taskfile.Merge( + vertex.Taskfile, + include, + ); err != nil { + return err + } + + return nil + }() + if err != nil { + return nil, err + } + // ) + } + + // Wait for all the go routines to finish + if err := g.Wait(); err != nil { + return nil, err + } + } + + // Get the root vertex + rootVertex, err := tfg.Vertex(hashes[0]) + if err != nil { + return nil, err + } + + rootVertex.Taskfile.Tasks.Range(func(name string, task *Task) error { + if task == nil { + task = &Task{} + rootVertex.Taskfile.Tasks.Set(name, task) + } + task.Task = name + return nil + }) + + return rootVertex.Taskfile, nil } diff --git a/taskfile/ast/taskfile.go b/taskfile/ast/taskfile.go index a05556c0..bbe1462f 100644 --- a/taskfile/ast/taskfile.go +++ b/taskfile/ast/taskfile.go @@ -1,6 +1,7 @@ package ast import ( + "errors" "fmt" "time" @@ -13,6 +14,9 @@ const NamespaceSeparator = ":" var V3 = semver.MustParse("3") +// ErrIncludedTaskfilesCantHaveDotenvs is returned when a included Taskfile contains dotenvs +var ErrIncludedTaskfilesCantHaveDotenvs = errors.New("task: Included Taskfiles can't have dotenv declarations. Please, move the dotenv declaration to the main Taskfile") + // Taskfile is the abstract syntax tree for a Taskfile type Taskfile struct { Location string @@ -36,6 +40,9 @@ func (t1 *Taskfile) Merge(t2 *Taskfile, include *Include) error { if !t1.Version.Equal(t2.Version) { return fmt.Errorf(`task: Taskfiles versions should match. First is "%s" but second is "%s"`, t1.Version, t2.Version) } + if len(t2.Dotenv) > 0 { + return ErrIncludedTaskfilesCantHaveDotenvs + } if t2.Output.IsSet() { t1.Output = t2.Output } diff --git a/taskfile/ast/tasks.go b/taskfile/ast/tasks.go index b6c85bb4..bdadb482 100644 --- a/taskfile/ast/tasks.go +++ b/taskfile/ast/tasks.go @@ -54,20 +54,25 @@ func (t1 *Tasks) Merge(t2 Tasks, include *Include) { // taskfile are marked as internal task.Internal = task.Internal || (include != nil && include.Internal) - // Add namespaces to dependencies, commands and aliases + // Add namespaces to task dependencies for _, dep := range task.Deps { if dep != nil && dep.Task != "" { dep.Task = taskNameWithNamespace(dep.Task, include.Namespace) } } + + // Add namespaces to task commands for _, cmd := range task.Cmds { if cmd != nil && cmd.Task != "" { cmd.Task = taskNameWithNamespace(cmd.Task, include.Namespace) } } + + // Add namespaces to task aliases for i, alias := range task.Aliases { task.Aliases[i] = taskNameWithNamespace(alias, include.Namespace) } + // Add namespace aliases if include != nil { for _, namespaceAlias := range include.Aliases { diff --git a/taskfile/reader.go b/taskfile/reader.go index 662a36fd..81841acd 100644 --- a/taskfile/reader.go +++ b/taskfile/reader.go @@ -140,7 +140,7 @@ func (r *Reader) include(node Node) error { } // Create an edge between the Taskfiles - err = r.graph.AddEdge(node.Location(), includeNode.Location()) + err = r.graph.AddEdge(node.Location(), includeNode.Location(), graph.EdgeData(include)) if errors.Is(err, graph.ErrEdgeAlreadyExists) { edge, err := r.graph.Edge(node.Location(), includeNode.Location()) if err != nil { diff --git a/taskfile/taskfile.go b/taskfile/taskfile.go index 499a79a5..ca915eae 100644 --- a/taskfile/taskfile.go +++ b/taskfile/taskfile.go @@ -16,9 +16,6 @@ import ( ) var ( - // ErrIncludedTaskfilesCantHaveDotenvs is returned when a included Taskfile contains dotenvs - ErrIncludedTaskfilesCantHaveDotenvs = errors.New("task: Included Taskfiles can't have dotenv declarations. Please, move the dotenv declaration to the main Taskfile") - defaultTaskfiles = []string{ "Taskfile.yml", "taskfile.yml", @@ -29,7 +26,6 @@ var ( "Taskfile.dist.yaml", "taskfile.dist.yaml", } - allowedContentTypes = []string{ "text/plain", "text/yaml",