aboutsummaryrefslogtreecommitdiffstatshomepage
path: root/cli/internal/graph
diff options
context:
space:
mode:
Diffstat (limited to 'cli/internal/graph')
-rw-r--r--cli/internal/graph/graph.go274
-rw-r--r--cli/internal/graph/graph_test.go50
2 files changed, 324 insertions, 0 deletions
diff --git a/cli/internal/graph/graph.go b/cli/internal/graph/graph.go
new file mode 100644
index 0000000..480dec9
--- /dev/null
+++ b/cli/internal/graph/graph.go
@@ -0,0 +1,274 @@
+// Package graph contains the CompleteGraph struct and some methods around it
+package graph
+
+import (
+ gocontext "context"
+ "fmt"
+ "path/filepath"
+ "regexp"
+ "sort"
+ "strings"
+
+ "github.com/hashicorp/go-hclog"
+ "github.com/pyr-sh/dag"
+ "github.com/vercel/turbo/cli/internal/env"
+ "github.com/vercel/turbo/cli/internal/fs"
+ "github.com/vercel/turbo/cli/internal/nodes"
+ "github.com/vercel/turbo/cli/internal/runsummary"
+ "github.com/vercel/turbo/cli/internal/taskhash"
+ "github.com/vercel/turbo/cli/internal/turbopath"
+ "github.com/vercel/turbo/cli/internal/util"
+ "github.com/vercel/turbo/cli/internal/workspace"
+)
+
+// CompleteGraph represents the common state inferred from the filesystem and pipeline.
+// It is not intended to include information specific to a particular run.
+type CompleteGraph struct {
+ // WorkspaceGraph expresses the dependencies between packages
+ WorkspaceGraph dag.AcyclicGraph
+
+ // Pipeline is config from turbo.json
+ Pipeline fs.Pipeline
+
+ // WorkspaceInfos stores the package.json contents by package name
+ WorkspaceInfos workspace.Catalog
+
+ // GlobalHash is the hash of all global dependencies
+ GlobalHash string
+
+ RootNode string
+
+ // Map of TaskDefinitions by taskID
+ TaskDefinitions map[string]*fs.TaskDefinition
+ RepoRoot turbopath.AbsoluteSystemPath
+
+ TaskHashTracker *taskhash.Tracker
+}
+
+// GetPackageTaskVisitor wraps a `visitor` function that is used for walking the TaskGraph
+// during execution (or dry-runs). The function returned here does not execute any tasks itself,
+// but it helps curry some data from the Complete Graph and pass it into the visitor function.
+func (g *CompleteGraph) GetPackageTaskVisitor(
+ ctx gocontext.Context,
+ taskGraph *dag.AcyclicGraph,
+ globalEnvMode util.EnvMode,
+ getArgs func(taskID string) []string,
+ logger hclog.Logger,
+ execFunc func(ctx gocontext.Context, packageTask *nodes.PackageTask, taskSummary *runsummary.TaskSummary) error,
+) func(taskID string) error {
+ return func(taskID string) error {
+ packageName, taskName := util.GetPackageTaskFromId(taskID)
+ pkg, ok := g.WorkspaceInfos.PackageJSONs[packageName]
+ if !ok {
+ return fmt.Errorf("cannot find package %v for task %v", packageName, taskID)
+ }
+
+ // Check for root task
+ var command string
+ if cmd, ok := pkg.Scripts[taskName]; ok {
+ command = cmd
+ }
+
+ if packageName == util.RootPkgName && commandLooksLikeTurbo(command) {
+ return fmt.Errorf("root task %v (%v) looks like it invokes turbo and might cause a loop", taskName, command)
+ }
+
+ taskDefinition, ok := g.TaskDefinitions[taskID]
+ if !ok {
+ return fmt.Errorf("Could not find definition for task")
+ }
+
+ // Task env mode is only independent when global env mode is `infer`.
+ taskEnvMode := globalEnvMode
+ useOldTaskHashable := false
+ if taskEnvMode == util.Infer {
+ if taskDefinition.PassthroughEnv != nil {
+ taskEnvMode = util.Strict
+ } else {
+ // If we're in infer mode we have just detected non-usage of strict env vars.
+ // Since we haven't stabilized this we don't want to break their cache.
+ useOldTaskHashable = true
+
+ // But our old behavior's actual meaning of this state is `loose`.
+ taskEnvMode = util.Loose
+ }
+ }
+
+ // TODO: maybe we can remove this PackageTask struct at some point
+ packageTask := &nodes.PackageTask{
+ TaskID: taskID,
+ Task: taskName,
+ PackageName: packageName,
+ Pkg: pkg,
+ EnvMode: taskEnvMode,
+ Dir: pkg.Dir.ToString(),
+ TaskDefinition: taskDefinition,
+ Outputs: taskDefinition.Outputs.Inclusions,
+ ExcludedOutputs: taskDefinition.Outputs.Exclusions,
+ }
+
+ passThruArgs := getArgs(taskName)
+ hash, err := g.TaskHashTracker.CalculateTaskHash(
+ packageTask,
+ taskGraph.DownEdges(taskID),
+ logger,
+ passThruArgs,
+ useOldTaskHashable,
+ )
+
+ // Not being able to construct the task hash is a hard error
+ if err != nil {
+ return fmt.Errorf("Hashing error: %v", err)
+ }
+
+ pkgDir := pkg.Dir
+ packageTask.Hash = hash
+ envVars := g.TaskHashTracker.GetEnvVars(taskID)
+ expandedInputs := g.TaskHashTracker.GetExpandedInputs(packageTask)
+ framework := g.TaskHashTracker.GetFramework(taskID)
+
+ logFile := repoRelativeLogFile(pkgDir, taskName)
+ packageTask.LogFile = logFile
+ packageTask.Command = command
+
+ var envVarPassthroughMap env.EnvironmentVariableMap
+ if taskDefinition.PassthroughEnv != nil {
+ if envVarPassthroughDetailedMap, err := env.GetHashableEnvVars(taskDefinition.PassthroughEnv, nil, ""); err == nil {
+ envVarPassthroughMap = envVarPassthroughDetailedMap.BySource.Explicit
+ }
+ }
+
+ summary := &runsummary.TaskSummary{
+ TaskID: taskID,
+ Task: taskName,
+ Hash: hash,
+ Package: packageName,
+ Dir: pkgDir.ToString(),
+ Outputs: taskDefinition.Outputs.Inclusions,
+ ExcludedOutputs: taskDefinition.Outputs.Exclusions,
+ LogFile: logFile,
+ ResolvedTaskDefinition: taskDefinition,
+ ExpandedInputs: expandedInputs,
+ ExpandedOutputs: []turbopath.AnchoredSystemPath{},
+ Command: command,
+ CommandArguments: passThruArgs,
+ Framework: framework,
+ EnvMode: taskEnvMode,
+ EnvVars: runsummary.TaskEnvVarSummary{
+ Configured: envVars.BySource.Explicit.ToSecretHashable(),
+ Inferred: envVars.BySource.Matching.ToSecretHashable(),
+ Passthrough: envVarPassthroughMap.ToSecretHashable(),
+ },
+ ExternalDepsHash: pkg.ExternalDepsHash,
+ }
+
+ if ancestors, err := g.getTaskGraphAncestors(taskGraph, packageTask.TaskID); err == nil {
+ summary.Dependencies = ancestors
+ }
+ if descendents, err := g.getTaskGraphDescendants(taskGraph, packageTask.TaskID); err == nil {
+ summary.Dependents = descendents
+ }
+
+ return execFunc(ctx, packageTask, summary)
+ }
+}
+
+// GetPipelineFromWorkspace returns the Unmarshaled fs.Pipeline struct from turbo.json in the given workspace.
+func (g *CompleteGraph) GetPipelineFromWorkspace(workspaceName string, isSinglePackage bool) (fs.Pipeline, error) {
+ turboConfig, err := g.GetTurboConfigFromWorkspace(workspaceName, isSinglePackage)
+
+ if err != nil {
+ return nil, err
+ }
+
+ return turboConfig.Pipeline, nil
+}
+
+// GetTurboConfigFromWorkspace returns the Unmarshaled fs.TurboJSON from turbo.json in the given workspace.
+func (g *CompleteGraph) GetTurboConfigFromWorkspace(workspaceName string, isSinglePackage bool) (*fs.TurboJSON, error) {
+ cachedTurboConfig, ok := g.WorkspaceInfos.TurboConfigs[workspaceName]
+
+ if ok {
+ return cachedTurboConfig, nil
+ }
+
+ var workspacePackageJSON *fs.PackageJSON
+ if pkgJSON, err := g.GetPackageJSONFromWorkspace(workspaceName); err == nil {
+ workspacePackageJSON = pkgJSON
+ } else {
+ return nil, err
+ }
+
+ // Note: pkgJSON.Dir for the root workspace will be an empty string, and for
+ // other workspaces, it will be a relative path.
+ workspaceAbsolutePath := workspacePackageJSON.Dir.RestoreAnchor(g.RepoRoot)
+ turboConfig, err := fs.LoadTurboConfig(workspaceAbsolutePath, workspacePackageJSON, isSinglePackage)
+
+ // If we failed to load a TurboConfig, bubble up the error
+ if err != nil {
+ return nil, err
+ }
+
+ // add to cache
+ g.WorkspaceInfos.TurboConfigs[workspaceName] = turboConfig
+
+ return g.WorkspaceInfos.TurboConfigs[workspaceName], nil
+}
+
+// GetPackageJSONFromWorkspace returns an Unmarshaled struct of the package.json in the given workspace
+func (g *CompleteGraph) GetPackageJSONFromWorkspace(workspaceName string) (*fs.PackageJSON, error) {
+ if pkgJSON, ok := g.WorkspaceInfos.PackageJSONs[workspaceName]; ok {
+ return pkgJSON, nil
+ }
+
+ return nil, fmt.Errorf("No package.json for %s", workspaceName)
+}
+
+// repoRelativeLogFile returns the path to the log file for this task execution as a
+// relative path from the root of the monorepo.
+func repoRelativeLogFile(dir turbopath.AnchoredSystemPath, taskName string) string {
+ return filepath.Join(dir.ToStringDuringMigration(), ".turbo", fmt.Sprintf("turbo-%v.log", taskName))
+}
+
+// getTaskGraphAncestors gets all the ancestors for a given task in the graph.
+// "ancestors" are all tasks that the given task depends on.
+func (g *CompleteGraph) getTaskGraphAncestors(taskGraph *dag.AcyclicGraph, taskID string) ([]string, error) {
+ ancestors, err := taskGraph.Ancestors(taskID)
+ if err != nil {
+ return nil, err
+ }
+ stringAncestors := []string{}
+ for _, dep := range ancestors {
+ // Don't leak out internal root node name, which are just placeholders
+ if !strings.Contains(dep.(string), g.RootNode) {
+ stringAncestors = append(stringAncestors, dep.(string))
+ }
+ }
+
+ sort.Strings(stringAncestors)
+ return stringAncestors, nil
+}
+
+// getTaskGraphDescendants gets all the descendants for a given task in the graph.
+// "descendants" are all tasks that depend on the given taskID.
+func (g *CompleteGraph) getTaskGraphDescendants(taskGraph *dag.AcyclicGraph, taskID string) ([]string, error) {
+ descendents, err := taskGraph.Descendents(taskID)
+ if err != nil {
+ return nil, err
+ }
+ stringDescendents := []string{}
+ for _, dep := range descendents {
+ // Don't leak out internal root node name, which are just placeholders
+ if !strings.Contains(dep.(string), g.RootNode) {
+ stringDescendents = append(stringDescendents, dep.(string))
+ }
+ }
+ sort.Strings(stringDescendents)
+ return stringDescendents, nil
+}
+
+var _isTurbo = regexp.MustCompile(`(?:^|\s)turbo(?:$|\s)`)
+
+func commandLooksLikeTurbo(command string) bool {
+ return _isTurbo.MatchString(command)
+}
diff --git a/cli/internal/graph/graph_test.go b/cli/internal/graph/graph_test.go
new file mode 100644
index 0000000..9323e19
--- /dev/null
+++ b/cli/internal/graph/graph_test.go
@@ -0,0 +1,50 @@
+package graph
+
+import (
+ "testing"
+
+ "gotest.tools/v3/assert"
+)
+
+func Test_CommandsInvokingTurbo(t *testing.T) {
+ type testCase struct {
+ command string
+ match bool
+ }
+ testCases := []testCase{
+ {
+ "turbo run foo",
+ true,
+ },
+ {
+ "rm -rf ~/Library/Caches/pnpm && turbo run foo && rm -rf ~/.npm",
+ true,
+ },
+ {
+ "FLAG=true turbo run foo",
+ true,
+ },
+ {
+ "npx turbo run foo",
+ true,
+ },
+ {
+ "echo starting; turbo foo; echo done",
+ true,
+ },
+ // We don't catch this as if people are going to try to invoke the turbo
+ // binary directly, they'll always be able to work around us.
+ {
+ "./node_modules/.bin/turbo foo",
+ false,
+ },
+ {
+ "rm -rf ~/Library/Caches/pnpm && rm -rf ~/Library/Caches/turbo && rm -rf ~/.npm && rm -rf ~/.pnpm-store && rm -rf ~/.turbo",
+ false,
+ },
+ }
+
+ for _, tc := range testCases {
+ assert.Equal(t, commandLooksLikeTurbo(tc.command), tc.match, tc.command)
+ }
+}