aboutsummaryrefslogtreecommitdiffstatshomepage
path: root/cli/internal/util
diff options
context:
space:
mode:
author简律纯 <hsiangnianian@outlook.com>2023-04-28 01:36:44 +0800
committer简律纯 <hsiangnianian@outlook.com>2023-04-28 01:36:44 +0800
commitdd84b9d64fb98746a230cd24233ff50a562c39c9 (patch)
treeb583261ef00b3afe72ec4d6dacb31e57779a6faf /cli/internal/util
parent0b46fcd72ac34382387b2bcf9095233efbcc52f4 (diff)
downloadHydroRoll-dd84b9d64fb98746a230cd24233ff50a562c39c9.tar.gz
HydroRoll-dd84b9d64fb98746a230cd24233ff50a562c39c9.zip
Diffstat (limited to 'cli/internal/util')
-rw-r--r--cli/internal/util/backends.go30
-rw-r--r--cli/internal/util/browser/open.go37
-rw-r--r--cli/internal/util/closer.go15
-rw-r--r--cli/internal/util/cmd.go24
-rw-r--r--cli/internal/util/filter/filter.go133
-rw-r--r--cli/internal/util/filter/filter_test.go116
-rw-r--r--cli/internal/util/graph.go35
-rw-r--r--cli/internal/util/modulo.go13
-rw-r--r--cli/internal/util/parse_concurrency.go39
-rw-r--r--cli/internal/util/parse_concurrency_test.go79
-rw-r--r--cli/internal/util/printf.go63
-rw-r--r--cli/internal/util/run_opts.go53
-rw-r--r--cli/internal/util/semaphore.go43
-rw-r--r--cli/internal/util/set.go147
-rw-r--r--cli/internal/util/set_test.go149
-rw-r--r--cli/internal/util/status.go47
-rw-r--r--cli/internal/util/task_id.go66
-rw-r--r--cli/internal/util/task_output_mode.go100
18 files changed, 1189 insertions, 0 deletions
diff --git a/cli/internal/util/backends.go b/cli/internal/util/backends.go
new file mode 100644
index 0000000..66941ad
--- /dev/null
+++ b/cli/internal/util/backends.go
@@ -0,0 +1,30 @@
+package util
+
+import (
+ "fmt"
+ "io/ioutil"
+ "path/filepath"
+
+ "github.com/vercel/turbo/cli/internal/yaml"
+)
+
+// YarnRC Represents contents of .yarnrc.yml
+type YarnRC struct {
+ NodeLinker string `yaml:"nodeLinker"`
+}
+
+// IsNMLinker Checks that Yarn is set to use the node-modules linker style
+func IsNMLinker(cwd string) (bool, error) {
+ yarnRC := &YarnRC{}
+
+ bytes, err := ioutil.ReadFile(filepath.Join(cwd, ".yarnrc.yml"))
+ if err != nil {
+ return false, fmt.Errorf(".yarnrc.yml: %w", err)
+ }
+
+ if yaml.Unmarshal(bytes, yarnRC) != nil {
+ return false, fmt.Errorf(".yarnrc.yml: %w", err)
+ }
+
+ return yarnRC.NodeLinker == "node-modules", nil
+}
diff --git a/cli/internal/util/browser/open.go b/cli/internal/util/browser/open.go
new file mode 100644
index 0000000..a6171e9
--- /dev/null
+++ b/cli/internal/util/browser/open.go
@@ -0,0 +1,37 @@
+package browser
+
+import (
+ "fmt"
+ "os/exec"
+ "runtime"
+)
+
+// OpenBrowser attempts to interactively open a browser window at the given URL
+func OpenBrowser(url string) error {
+ var err error
+
+ switch runtime.GOOS {
+ case "linux":
+ if posixBinExists("wslview") {
+ err = exec.Command("wslview", url).Start()
+ } else {
+ err = exec.Command("xdg-open", url).Start()
+ }
+ case "windows":
+ err = exec.Command("rundll32", "url.dll,FileProtocolHandler", url).Start()
+ case "darwin":
+ err = exec.Command("open", url).Start()
+ default:
+ err = fmt.Errorf("unsupported platform")
+ }
+ if err != nil {
+ return err
+ }
+ return nil
+}
+
+func posixBinExists(bin string) bool {
+ err := exec.Command("which", bin).Run()
+ // we mostly don't care what the error is, it suggests the binary is not usable
+ return err == nil
+}
diff --git a/cli/internal/util/closer.go b/cli/internal/util/closer.go
new file mode 100644
index 0000000..996760b
--- /dev/null
+++ b/cli/internal/util/closer.go
@@ -0,0 +1,15 @@
+package util
+
+// CloseAndIgnoreError is a utility to tell our linter that we explicitly deem it okay
+// to not check a particular error on closing of a resource.
+//
+// We use `errcheck` as a linter, which is super-opinionated about checking errors,
+// even in places where we don't necessarily care to check the error.
+//
+// `golangci-lint` has a default ignore list for this lint problem (EXC0001) which
+// can be used to sidestep this problem but it's possibly a little too-heavy-handed
+// in exclusion. At the expense of discoverability, this utility function forces
+// opt-in to ignoring errors on closing of things that can be `Close`d.
+func CloseAndIgnoreError(closer interface{ Close() error }) {
+ _ = closer.Close()
+}
diff --git a/cli/internal/util/cmd.go b/cli/internal/util/cmd.go
new file mode 100644
index 0000000..ae79aa0
--- /dev/null
+++ b/cli/internal/util/cmd.go
@@ -0,0 +1,24 @@
+package util
+
+import (
+ "bytes"
+
+ "github.com/spf13/cobra"
+)
+
+// ExitCodeError is a specific error that is returned by the command to specify the exit code
+type ExitCodeError struct {
+ ExitCode int
+}
+
+func (e *ExitCodeError) Error() string { return "exit code error" }
+
+// HelpForCobraCmd returns the help string for a given command
+// Note that this overwrites the output for the command
+func HelpForCobraCmd(cmd *cobra.Command) string {
+ f := cmd.HelpFunc()
+ buf := bytes.NewBufferString("")
+ cmd.SetOut(buf)
+ f(cmd, []string{})
+ return buf.String()
+}
diff --git a/cli/internal/util/filter/filter.go b/cli/internal/util/filter/filter.go
new file mode 100644
index 0000000..fbc475d
--- /dev/null
+++ b/cli/internal/util/filter/filter.go
@@ -0,0 +1,133 @@
+// Copyright (c) 2015-2020 InfluxData Inc. MIT License (MIT)
+// https://github.com/influxdata/telegraf
+package filter
+
+import (
+ "strings"
+
+ "github.com/gobwas/glob"
+)
+
+type Filter interface {
+ Match(string) bool
+}
+
+// Compile takes a list of string filters and returns a Filter interface
+// for matching a given string against the filter list. The filter list
+// supports glob matching too, ie:
+//
+// f, _ := Compile([]string{"cpu", "mem", "net*"})
+// f.Match("cpu") // true
+// f.Match("network") // true
+// f.Match("memory") // false
+func Compile(filters []string) (Filter, error) {
+ // return if there is nothing to compile
+ if len(filters) == 0 {
+ return nil, nil
+ }
+
+ // check if we can compile a non-glob filter
+ noGlob := true
+ for _, filter := range filters {
+ if hasMeta(filter) {
+ noGlob = false
+ break
+ }
+ }
+
+ switch {
+ case noGlob:
+ // return non-globbing filter if not needed.
+ return compileFilterNoGlob(filters), nil
+ case len(filters) == 1:
+ return glob.Compile(filters[0])
+ default:
+ return glob.Compile("{" + strings.Join(filters, ",") + "}")
+ }
+}
+
+// hasMeta reports whether path contains any magic glob characters.
+func hasMeta(s string) bool {
+ return strings.ContainsAny(s, "*?[")
+}
+
+type filter struct {
+ m map[string]struct{}
+}
+
+func (f *filter) Match(s string) bool {
+ _, ok := f.m[s]
+ return ok
+}
+
+type filtersingle struct {
+ s string
+}
+
+func (f *filtersingle) Match(s string) bool {
+ return f.s == s
+}
+
+func compileFilterNoGlob(filters []string) Filter {
+ if len(filters) == 1 {
+ return &filtersingle{s: filters[0]}
+ }
+ out := filter{m: make(map[string]struct{})}
+ for _, filter := range filters {
+ out.m[filter] = struct{}{}
+ }
+ return &out
+}
+
+type IncludeExcludeFilter struct {
+ include Filter
+ exclude Filter
+ includeDefault bool
+ excludeDefault bool
+}
+
+func NewIncludeExcludeFilter(
+ include []string,
+ exclude []string,
+) (Filter, error) {
+ return NewIncludeExcludeFilterDefaults(include, exclude, true, false)
+}
+
+func NewIncludeExcludeFilterDefaults(
+ include []string,
+ exclude []string,
+ includeDefault bool,
+ excludeDefault bool,
+) (Filter, error) {
+ in, err := Compile(include)
+ if err != nil {
+ return nil, err
+ }
+
+ ex, err := Compile(exclude)
+ if err != nil {
+ return nil, err
+ }
+
+ return &IncludeExcludeFilter{in, ex, includeDefault, excludeDefault}, nil
+}
+
+func (f *IncludeExcludeFilter) Match(s string) bool {
+ if f.include != nil {
+ if !f.include.Match(s) {
+ return false
+ }
+ } else if !f.includeDefault {
+ return false
+ }
+
+ if f.exclude != nil {
+ if f.exclude.Match(s) {
+ return false
+ }
+ } else if f.excludeDefault {
+ return false
+ }
+
+ return true
+}
diff --git a/cli/internal/util/filter/filter_test.go b/cli/internal/util/filter/filter_test.go
new file mode 100644
index 0000000..727a4b6
--- /dev/null
+++ b/cli/internal/util/filter/filter_test.go
@@ -0,0 +1,116 @@
+// Copyright (c) 2015-2020 InfluxData Inc. MIT License (MIT)
+// https://github.com/influxdata/telegraf
+package filter
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestCompile(t *testing.T) {
+ f, err := Compile([]string{})
+ assert.NoError(t, err)
+ assert.Nil(t, f)
+
+ f, err = Compile([]string{"cpu"})
+ assert.NoError(t, err)
+ assert.True(t, f.Match("cpu"))
+ assert.False(t, f.Match("cpu0"))
+ assert.False(t, f.Match("mem"))
+
+ f, err = Compile([]string{"cpu*"})
+ assert.NoError(t, err)
+ assert.True(t, f.Match("cpu"))
+ assert.True(t, f.Match("cpu0"))
+ assert.False(t, f.Match("mem"))
+
+ f, err = Compile([]string{"cpu", "mem"})
+ assert.NoError(t, err)
+ assert.True(t, f.Match("cpu"))
+ assert.False(t, f.Match("cpu0"))
+ assert.True(t, f.Match("mem"))
+
+ f, err = Compile([]string{"cpu", "mem", "net*"})
+ assert.NoError(t, err)
+ assert.True(t, f.Match("cpu"))
+ assert.False(t, f.Match("cpu0"))
+ assert.True(t, f.Match("mem"))
+ assert.True(t, f.Match("network"))
+}
+
+func TestIncludeExclude(t *testing.T) {
+ tags := []string{}
+ labels := []string{"best", "com_influxdata", "timeseries", "com_influxdata_telegraf", "ever"}
+
+ filter, err := NewIncludeExcludeFilter([]string{}, []string{"com_influx*"})
+ if err != nil {
+ t.Fatalf("Failed to create include/exclude filter - %v", err)
+ }
+
+ for i := range labels {
+ if filter.Match(labels[i]) {
+ tags = append(tags, labels[i])
+ }
+ }
+
+ assert.Equal(t, []string{"best", "timeseries", "ever"}, tags)
+}
+
+var benchbool bool
+
+func BenchmarkFilterSingleNoGlobFalse(b *testing.B) {
+ f, _ := Compile([]string{"cpu"})
+ var tmp bool
+ for n := 0; n < b.N; n++ {
+ tmp = f.Match("network")
+ }
+ benchbool = tmp
+}
+
+func BenchmarkFilterSingleNoGlobTrue(b *testing.B) {
+ f, _ := Compile([]string{"cpu"})
+ var tmp bool
+ for n := 0; n < b.N; n++ {
+ tmp = f.Match("cpu")
+ }
+ benchbool = tmp
+}
+
+func BenchmarkFilter(b *testing.B) {
+ f, _ := Compile([]string{"cpu", "mem", "net*"})
+ var tmp bool
+ for n := 0; n < b.N; n++ {
+ tmp = f.Match("network")
+ }
+ benchbool = tmp
+}
+
+func BenchmarkFilterNoGlob(b *testing.B) {
+ f, _ := Compile([]string{"cpu", "mem", "net"})
+ var tmp bool
+ for n := 0; n < b.N; n++ {
+ tmp = f.Match("net")
+ }
+ benchbool = tmp
+}
+
+func BenchmarkFilter2(b *testing.B) {
+ f, _ := Compile([]string{"aa", "bb", "c", "ad", "ar", "at", "aq",
+ "aw", "az", "axxx", "ab", "cpu", "mem", "net*"})
+ var tmp bool
+ for n := 0; n < b.N; n++ {
+ tmp = f.Match("network")
+ }
+ benchbool = tmp
+}
+
+func BenchmarkFilter2NoGlob(b *testing.B) {
+ f, _ := Compile([]string{"aa", "bb", "c", "ad", "ar", "at", "aq",
+ "aw", "az", "axxx", "ab", "cpu", "mem", "net"})
+ var tmp bool
+ for n := 0; n < b.N; n++ {
+ tmp = f.Match("net")
+ }
+ benchbool = tmp
+}
diff --git a/cli/internal/util/graph.go b/cli/internal/util/graph.go
new file mode 100644
index 0000000..89de18c
--- /dev/null
+++ b/cli/internal/util/graph.go
@@ -0,0 +1,35 @@
+package util
+
+import (
+ "fmt"
+ "strings"
+
+ "github.com/pyr-sh/dag"
+)
+
+// ValidateGraph checks that a given DAG has no cycles and no self-referential edges.
+// We differ from the underlying DAG Validate method in that we allow multiple roots.
+func ValidateGraph(graph *dag.AcyclicGraph) error {
+ // We use Cycles instead of Validate because
+ // our DAG has multiple roots (entrypoints).
+ // Validate mandates that there is only a single root node.
+ cycles := graph.Cycles()
+ if len(cycles) > 0 {
+ cycleLines := make([]string, len(cycles))
+ for i, cycle := range cycles {
+ vertices := make([]string, len(cycle))
+ for j, vertex := range cycle {
+ vertices[j] = vertex.(string)
+ }
+ cycleLines[i] = "\t" + strings.Join(vertices, ",")
+ }
+ return fmt.Errorf("cyclic dependency detected:\n%s", strings.Join(cycleLines, "\n"))
+ }
+
+ for _, e := range graph.Edges() {
+ if e.Source() == e.Target() {
+ return fmt.Errorf("%s depends on itself", e.Source())
+ }
+ }
+ return nil
+}
diff --git a/cli/internal/util/modulo.go b/cli/internal/util/modulo.go
new file mode 100644
index 0000000..ec2957a
--- /dev/null
+++ b/cli/internal/util/modulo.go
@@ -0,0 +1,13 @@
+package util
+
+// PostitiveMod returns a modulo operator like JavaScripts
+func PositiveMod(x, d int) int {
+ x = x % d
+ if x >= 0 {
+ return x
+ }
+ if d < 0 {
+ return x - d
+ }
+ return x + d
+}
diff --git a/cli/internal/util/parse_concurrency.go b/cli/internal/util/parse_concurrency.go
new file mode 100644
index 0000000..6917600
--- /dev/null
+++ b/cli/internal/util/parse_concurrency.go
@@ -0,0 +1,39 @@
+package util
+
+import (
+ "fmt"
+ "math"
+ "runtime"
+ "strconv"
+ "strings"
+)
+
+var (
+ // alias so we can mock in tests
+ runtimeNumCPU = runtime.NumCPU
+ // positive values check for +Inf
+ _positiveInfinity = 1
+)
+
+// ParseConcurrency parses a concurrency value, which can be a number (e.g. 2) or a percentage (e.g. 50%).
+func ParseConcurrency(concurrencyRaw string) (int, error) {
+ if strings.HasSuffix(concurrencyRaw, "%") {
+ if percent, err := strconv.ParseFloat(concurrencyRaw[:len(concurrencyRaw)-1], 64); err != nil {
+ return 0, fmt.Errorf("invalid value for --concurrency CLI flag. This should be a number --concurrency=4 or percentage of CPU cores --concurrency=50%% : %w", err)
+ } else {
+ if percent > 0 && !math.IsInf(percent, _positiveInfinity) {
+ return int(math.Max(1, float64(runtimeNumCPU())*percent/100)), nil
+ } else {
+ return 0, fmt.Errorf("invalid percentage value for --concurrency CLI flag. This should be a percentage of CPU cores, between 1%% and 100%% : %w", err)
+ }
+ }
+ } else if i, err := strconv.Atoi(concurrencyRaw); err != nil {
+ return 0, fmt.Errorf("invalid value for --concurrency CLI flag. This should be a positive integer greater than or equal to 1: %w", err)
+ } else {
+ if i >= 1 {
+ return i, nil
+ } else {
+ return 0, fmt.Errorf("invalid value %v for --concurrency CLI flag. This should be a positive integer greater than or equal to 1", i)
+ }
+ }
+}
diff --git a/cli/internal/util/parse_concurrency_test.go b/cli/internal/util/parse_concurrency_test.go
new file mode 100644
index 0000000..b732724
--- /dev/null
+++ b/cli/internal/util/parse_concurrency_test.go
@@ -0,0 +1,79 @@
+package util
+
+import (
+ "fmt"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestParseConcurrency(t *testing.T) {
+ cases := []struct {
+ Input string
+ Expected int
+ }{
+ {
+ "12",
+ 12,
+ },
+ {
+ "200%",
+ 20,
+ },
+ {
+ "100%",
+ 10,
+ },
+ {
+ "50%",
+ 5,
+ },
+ {
+ "25%",
+ 2,
+ },
+ {
+ "1%",
+ 1,
+ },
+ {
+ "0644", // we parse in base 10
+ 644,
+ },
+ }
+
+ // mock runtime.NumCPU() to 10
+ runtimeNumCPU = func() int {
+ return 10
+ }
+
+ for i, tc := range cases {
+ t.Run(fmt.Sprintf("%d) '%s' should be parsed at '%d'", i, tc.Input, tc.Expected), func(t *testing.T) {
+ if result, err := ParseConcurrency(tc.Input); err != nil {
+ t.Fatalf("invalid parse: %#v", err)
+ } else {
+ assert.EqualValues(t, tc.Expected, result)
+ }
+ })
+ }
+}
+
+func TestInvalidPercents(t *testing.T) {
+ inputs := []string{
+ "asdf",
+ "-1",
+ "-l%",
+ "infinity%",
+ "-infinity%",
+ "nan%",
+ "0b01",
+ "0o644",
+ "0xFF",
+ }
+ for _, tc := range inputs {
+ t.Run(tc, func(t *testing.T) {
+ val, err := ParseConcurrency(tc)
+ assert.Error(t, err, "input %v got %v", tc, val)
+ })
+ }
+}
diff --git a/cli/internal/util/printf.go b/cli/internal/util/printf.go
new file mode 100644
index 0000000..9cd6dce
--- /dev/null
+++ b/cli/internal/util/printf.go
@@ -0,0 +1,63 @@
+// Copyright Thought Machine, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+package util
+
+import (
+ "fmt"
+ "io"
+ "os"
+
+ "github.com/vercel/turbo/cli/internal/ui"
+)
+
+// initPrintf sets up the replacements used by printf.
+func InitPrintf() {
+ if !ui.IsTTY {
+ replacements = map[string]string{}
+ }
+}
+
+// printf is used throughout this package to print something to stderr with some
+// replacements for pseudo-shell variables for ANSI formatting codes.
+func Sprintf(format string, args ...interface{}) string {
+ return os.Expand(fmt.Sprintf(format, args...), replace)
+}
+
+func Printf(format string, args ...interface{}) {
+ fmt.Fprint(os.Stderr, os.Expand(fmt.Sprintf(format, args...), replace))
+}
+
+func Fprintf(writer io.Writer, format string, args ...interface{}) {
+ fmt.Fprint(writer, os.Expand(fmt.Sprintf(format, args...), replace))
+}
+
+func replace(s string) string {
+ return replacements[s]
+}
+
+// These are the standard set of replacements we use.
+var replacements = map[string]string{
+ "BOLD": "\x1b[1m",
+ "BOLD_GREY": "\x1b[30;1m",
+ "BOLD_RED": "\x1b[31;1m",
+ "BOLD_GREEN": "\x1b[32;1m",
+ "BOLD_YELLOW": "\x1b[33;1m",
+ "BOLD_BLUE": "\x1b[34;1m",
+ "BOLD_MAGENTA": "\x1b[35;1m",
+ "BOLD_CYAN": "\x1b[36;1m",
+ "BOLD_WHITE": "\x1b[37;1m",
+ "UNDERLINE": "\x1b[4m",
+ "GREY": "\x1b[2m",
+ "RED": "\x1b[31m",
+ "GREEN": "\x1b[32m",
+ "YELLOW": "\x1b[33m",
+ "BLUE": "\x1b[34m",
+ "MAGENTA": "\x1b[35m",
+ "CYAN": "\x1b[36m",
+ "WHITE": "\x1b[37m",
+ "WHITE_ON_RED": "\x1b[37;41;1m",
+ "RED_NO_BG": "\x1b[31;49;1m",
+ "RESET": "\x1b[0m",
+ "ERASE_AFTER": "\x1b[K",
+ "CLEAR_END": "\x1b[0J",
+}
diff --git a/cli/internal/util/run_opts.go b/cli/internal/util/run_opts.go
new file mode 100644
index 0000000..08676a0
--- /dev/null
+++ b/cli/internal/util/run_opts.go
@@ -0,0 +1,53 @@
+package util
+
+import "strings"
+
+// EnvMode specifies if we will be using strict env vars
+type EnvMode string
+
+const (
+ // Infer - infer environment variable constraints from turbo.json
+ Infer EnvMode = "Infer"
+ // Loose - environment variables are unconstrained
+ Loose EnvMode = "Loose"
+ // Strict - environment variables are limited
+ Strict EnvMode = "Strict"
+)
+
+// MarshalText implements TextMarshaler for the struct.
+func (s EnvMode) MarshalText() (text []byte, err error) {
+ return []byte(strings.ToLower(string(s))), nil
+}
+
+// RunOpts holds the options that control the execution of a turbo run
+type RunOpts struct {
+ // Force execution to be serially one-at-a-time
+ Concurrency int
+ // Whether to execute in parallel (defaults to false)
+ Parallel bool
+
+ EnvMode EnvMode
+ // The filename to write a perf profile.
+ Profile string
+ // If true, continue task executions even if a task fails.
+ ContinueOnError bool
+ PassThroughArgs []string
+ // Restrict execution to only the listed task names. Default false
+ Only bool
+ // Dry run flags
+ DryRun bool
+ DryRunJSON bool
+ // Graph flags
+ GraphDot bool
+ GraphFile string
+ NoDaemon bool
+ SinglePackage bool
+
+ // logPrefix controls whether we should print a prefix in task logs
+ LogPrefix string
+
+ // Whether turbo should create a run summary
+ Summarize bool
+
+ ExperimentalSpaceID string
+}
diff --git a/cli/internal/util/semaphore.go b/cli/internal/util/semaphore.go
new file mode 100644
index 0000000..ef29df0
--- /dev/null
+++ b/cli/internal/util/semaphore.go
@@ -0,0 +1,43 @@
+package util
+
+// Semaphore is a wrapper around a channel to provide
+// utility methods to clarify that we are treating the
+// channel as a semaphore
+type Semaphore chan struct{}
+
+// NewSemaphore creates a semaphore that allows up
+// to a given limit of simultaneous acquisitions
+func NewSemaphore(n int) Semaphore {
+ if n <= 0 {
+ panic("semaphore with limit <=0")
+ }
+ ch := make(chan struct{}, n)
+ return Semaphore(ch)
+}
+
+// Acquire is used to acquire an available slot.
+// Blocks until available.
+func (s Semaphore) Acquire() {
+ s <- struct{}{}
+}
+
+// TryAcquire is used to do a non-blocking acquire.
+// Returns a bool indicating success
+func (s Semaphore) TryAcquire() bool {
+ select {
+ case s <- struct{}{}:
+ return true
+ default:
+ return false
+ }
+}
+
+// Release is used to return a slot. Acquire must
+// be called as a pre-condition.
+func (s Semaphore) Release() {
+ select {
+ case <-s:
+ default:
+ panic("release without an acquire")
+ }
+}
diff --git a/cli/internal/util/set.go b/cli/internal/util/set.go
new file mode 100644
index 0000000..b6c5f86
--- /dev/null
+++ b/cli/internal/util/set.go
@@ -0,0 +1,147 @@
+package util
+
+// Set is a set data structure.
+type Set map[interface{}]interface{}
+
+// SetFromStrings creates a Set containing the strings from the given slice
+func SetFromStrings(sl []string) Set {
+ set := make(Set, len(sl))
+ for _, item := range sl {
+ set.Add(item)
+ }
+ return set
+}
+
+// Hashable is the interface used by set to get the hash code of a value.
+// If this isn't given, then the value of the item being added to the set
+// itself is used as the comparison value.
+type Hashable interface {
+ Hashcode() interface{}
+}
+
+// hashcode returns the hashcode used for set elements.
+func hashcode(v interface{}) interface{} {
+ if h, ok := v.(Hashable); ok {
+ return h.Hashcode()
+ }
+
+ return v
+}
+
+// Add adds an item to the set
+func (s Set) Add(v interface{}) {
+ s[hashcode(v)] = v
+}
+
+// Delete removes an item from the set.
+func (s Set) Delete(v interface{}) {
+ delete(s, hashcode(v))
+}
+
+// Includes returns true/false of whether a value is in the set.
+func (s Set) Includes(v interface{}) bool {
+ _, ok := s[hashcode(v)]
+ return ok
+}
+
+// Intersection computes the set intersection with other.
+func (s Set) Intersection(other Set) Set {
+ result := make(Set)
+ if s == nil || other == nil {
+ return result
+ }
+ // Iteration over a smaller set has better performance.
+ if other.Len() < s.Len() {
+ s, other = other, s
+ }
+ for _, v := range s {
+ if other.Includes(v) {
+ result.Add(v)
+ }
+ }
+ return result
+}
+
+// Difference returns a set with the elements that s has but
+// other doesn't.
+func (s Set) Difference(other Set) Set {
+ result := make(Set)
+ for k, v := range s {
+ var ok bool
+ if other != nil {
+ _, ok = other[k]
+ }
+ if !ok {
+ result.Add(v)
+ }
+ }
+
+ return result
+}
+
+// Some tests whether at least one element in the array passes the test implemented by the provided function.
+// It returns a Boolean value.
+func (s Set) Some(cb func(interface{}) bool) bool {
+ for _, v := range s {
+ if cb(v) {
+ return true
+ }
+ }
+ return false
+}
+
+// Filter returns a set that contains the elements from the receiver
+// where the given callback returns true.
+func (s Set) Filter(cb func(interface{}) bool) Set {
+ result := make(Set)
+
+ for _, v := range s {
+ if cb(v) {
+ result.Add(v)
+ }
+ }
+
+ return result
+}
+
+// Len is the number of items in the set.
+func (s Set) Len() int {
+ return len(s)
+}
+
+// List returns the list of set elements.
+func (s Set) List() []interface{} {
+ if s == nil {
+ return nil
+ }
+
+ r := make([]interface{}, 0, len(s))
+ for _, v := range s {
+ r = append(r, v)
+ }
+
+ return r
+}
+
+// UnsafeListOfStrings dangerously casts list to a string
+func (s Set) UnsafeListOfStrings() []string {
+ if s == nil {
+ return nil
+ }
+
+ r := make([]string, 0, len(s))
+ for _, v := range s {
+ r = append(r, v.(string))
+ }
+
+ return r
+}
+
+// Copy returns a shallow copy of the set.
+func (s Set) Copy() Set {
+ c := make(Set)
+ for k, v := range s {
+ c[k] = v
+ }
+ return c
+}
diff --git a/cli/internal/util/set_test.go b/cli/internal/util/set_test.go
new file mode 100644
index 0000000..52736b4
--- /dev/null
+++ b/cli/internal/util/set_test.go
@@ -0,0 +1,149 @@
+package util
+
+import (
+ "fmt"
+ "testing"
+)
+
+func TestSetDifference(t *testing.T) {
+ cases := []struct {
+ Name string
+ A, B []interface{}
+ Expected []interface{}
+ }{
+ {
+ "same",
+ []interface{}{1, 2, 3},
+ []interface{}{3, 1, 2},
+ []interface{}{},
+ },
+
+ {
+ "A has extra elements",
+ []interface{}{1, 2, 3},
+ []interface{}{3, 2},
+ []interface{}{1},
+ },
+
+ {
+ "B has extra elements",
+ []interface{}{1, 2, 3},
+ []interface{}{3, 2, 1, 4},
+ []interface{}{},
+ },
+ }
+
+ for i, tc := range cases {
+ t.Run(fmt.Sprintf("%d-%s", i, tc.Name), func(t *testing.T) {
+ one := make(Set)
+ two := make(Set)
+ expected := make(Set)
+ for _, v := range tc.A {
+ one.Add(v)
+ }
+ for _, v := range tc.B {
+ two.Add(v)
+ }
+ for _, v := range tc.Expected {
+ expected.Add(v)
+ }
+
+ actual := one.Difference(two)
+ match := actual.Intersection(expected)
+ if match.Len() != expected.Len() {
+ t.Fatalf("bad: %#v", actual.List())
+ }
+ })
+ }
+}
+
+func TestSetFilter(t *testing.T) {
+ cases := []struct {
+ Input []interface{}
+ Expected []interface{}
+ }{
+ {
+ []interface{}{1, 2, 3},
+ []interface{}{1, 2, 3},
+ },
+
+ {
+ []interface{}{4, 5, 6},
+ []interface{}{4},
+ },
+
+ {
+ []interface{}{7, 8, 9},
+ []interface{}{},
+ },
+ }
+
+ for i, tc := range cases {
+ t.Run(fmt.Sprintf("%d-%#v", i, tc.Input), func(t *testing.T) {
+ input := make(Set)
+ expected := make(Set)
+ for _, v := range tc.Input {
+ input.Add(v)
+ }
+ for _, v := range tc.Expected {
+ expected.Add(v)
+ }
+
+ actual := input.Filter(func(v interface{}) bool {
+ return v.(int) < 5
+ })
+ match := actual.Intersection(expected)
+ if match.Len() != expected.Len() {
+ t.Fatalf("bad: %#v", actual.List())
+ }
+ })
+ }
+}
+
+func TestSetCopy(t *testing.T) {
+ a := make(Set)
+ a.Add(1)
+ a.Add(2)
+
+ b := a.Copy()
+ b.Add(3)
+
+ diff := b.Difference(a)
+
+ if diff.Len() != 1 {
+ t.Fatalf("expected single diff value, got %#v", diff)
+ }
+
+ if !diff.Includes(3) {
+ t.Fatalf("diff does not contain 3, got %#v", diff)
+ }
+
+}
+
+func makeSet(n int) Set {
+ ret := make(Set, n)
+ for i := 0; i < n; i++ {
+ ret.Add(i)
+ }
+ return ret
+}
+
+func BenchmarkSetIntersection_100_100000(b *testing.B) {
+ small := makeSet(100)
+ large := makeSet(100000)
+
+ b.ResetTimer()
+ for n := 0; n < b.N; n++ {
+ small.Intersection(large)
+ }
+}
+
+func BenchmarkSetIntersection_100000_100(b *testing.B) {
+ small := makeSet(100)
+ large := makeSet(100000)
+
+ b.ResetTimer()
+ for n := 0; n < b.N; n++ {
+ large.Intersection(small)
+ }
+}
diff --git a/cli/internal/util/status.go b/cli/internal/util/status.go
new file mode 100644
index 0000000..23ae165
--- /dev/null
+++ b/cli/internal/util/status.go
@@ -0,0 +1,47 @@
+package util
+
+import "fmt"
+
+// CachingStatus represents the api server's perspective
+// on whether remote caching should be allowed
+type CachingStatus int
+
+const (
+ // CachingStatusDisabled indicates that the server will not accept or serve artifacts
+ CachingStatusDisabled CachingStatus = iota
+ // CachingStatusEnabled indicates that the server will accept and serve artifacts
+ CachingStatusEnabled
+ // CachingStatusOverLimit indicates that a usage limit has been hit and the
+ // server will temporarily not accept or serve artifacts
+ CachingStatusOverLimit
+ // CachingStatusPaused indicates that a customer's spending has been paused and the
+ // server will temporarily not accept or serve artifacts
+ CachingStatusPaused
+)
+
+// CachingStatusFromString parses a raw string to a caching status enum value
+func CachingStatusFromString(raw string) (CachingStatus, error) {
+ switch raw {
+ case "disabled":
+ return CachingStatusDisabled, nil
+ case "enabled":
+ return CachingStatusEnabled, nil
+ case "over_limit":
+ return CachingStatusOverLimit, nil
+ case "paused":
+ return CachingStatusPaused, nil
+ default:
+ return CachingStatusDisabled, fmt.Errorf("unknown caching status: %v", raw)
+ }
+}
+
+// CacheDisabledError is an error used to indicate that remote caching
+// is not available.
+type CacheDisabledError struct {
+ Status CachingStatus
+ Message string
+}
+
+func (cd *CacheDisabledError) Error() string {
+ return cd.Message
+}
diff --git a/cli/internal/util/task_id.go b/cli/internal/util/task_id.go
new file mode 100644
index 0000000..e4415b6
--- /dev/null
+++ b/cli/internal/util/task_id.go
@@ -0,0 +1,66 @@
+package util
+
+import (
+ "fmt"
+ "strings"
+)
+
+const (
+ // TaskDelimiter separates a package name from a task name in a task id
+ TaskDelimiter = "#"
+ // RootPkgName is the reserved name that specifies the root package
+ RootPkgName = "//"
+)
+
+// GetTaskId returns a package-task identifier (e.g @feed/thing#build).
+func GetTaskId(pkgName interface{}, target string) string {
+ if IsPackageTask(target) {
+ return target
+ }
+ return fmt.Sprintf("%v%v%v", pkgName, TaskDelimiter, target)
+}
+
+// RootTaskID returns the task id for running the given task in the root package
+func RootTaskID(target string) string {
+ return GetTaskId(RootPkgName, target)
+}
+
+// GetPackageTaskFromId returns a tuple of the package name and target task
+func GetPackageTaskFromId(taskId string) (packageName string, task string) {
+ arr := strings.Split(taskId, TaskDelimiter)
+ return arr[0], arr[1]
+}
+
+// RootTaskTaskName returns the task portion of a root task taskID
+func RootTaskTaskName(taskID string) string {
+ return strings.TrimPrefix(taskID, RootPkgName+TaskDelimiter)
+}
+
+// IsPackageTask returns true if input is a package-specific task
+// whose name has a length greater than 0.
+//
+// Accepted: myapp#build
+// Rejected: #build, build
+func IsPackageTask(task string) bool {
+ return strings.Index(task, TaskDelimiter) > 0
+}
+
+// IsTaskInPackage returns true if the task does not belong to a different package
+// note that this means unscoped tasks will always return true
+func IsTaskInPackage(task string, packageName string) bool {
+ if !IsPackageTask(task) {
+ return true
+ }
+ packageNameExpected, _ := GetPackageTaskFromId(task)
+ return packageNameExpected == packageName
+}
+
+// StripPackageName removes the package portion of a taskID if it
+// is a package task. Non-package tasks are returned unmodified
+func StripPackageName(taskID string) string {
+ if IsPackageTask(taskID) {
+ _, task := GetPackageTaskFromId(taskID)
+ return task
+ }
+ return taskID
+}
diff --git a/cli/internal/util/task_output_mode.go b/cli/internal/util/task_output_mode.go
new file mode 100644
index 0000000..eee42e0
--- /dev/null
+++ b/cli/internal/util/task_output_mode.go
@@ -0,0 +1,100 @@
+package util
+
+import (
+ "encoding/json"
+ "fmt"
+)
+
+// TaskOutputMode defines the ways turbo can display task output during a run
+type TaskOutputMode int
+
+const (
+ // FullTaskOutput will show all task output
+ FullTaskOutput TaskOutputMode = iota
+ // NoTaskOutput will hide all task output
+ NoTaskOutput
+ // HashTaskOutput will display turbo-computed task hashes
+ HashTaskOutput
+ // NewTaskOutput will show all new task output and turbo-computed task hashes for cached output
+ NewTaskOutput
+ // ErrorTaskOutput will show task output for failures only; no cache miss/hit messages are emitted
+ ErrorTaskOutput
+)
+
+const (
+ fullTaskOutputString = "full"
+ noTaskOutputString = "none"
+ hashTaskOutputString = "hash-only"
+ newTaskOutputString = "new-only"
+ errorTaskOutputString = "errors-only"
+)
+
+// TaskOutputModeStrings is an array containing the string representations for task output modes
+var TaskOutputModeStrings = []string{
+ fullTaskOutputString,
+ noTaskOutputString,
+ hashTaskOutputString,
+ newTaskOutputString,
+ errorTaskOutputString,
+}
+
+// FromTaskOutputModeString converts a task output mode's string representation into the enum value
+func FromTaskOutputModeString(value string) (TaskOutputMode, error) {
+ switch value {
+ case fullTaskOutputString:
+ return FullTaskOutput, nil
+ case noTaskOutputString:
+ return NoTaskOutput, nil
+ case hashTaskOutputString:
+ return HashTaskOutput, nil
+ case newTaskOutputString:
+ return NewTaskOutput, nil
+ case errorTaskOutputString:
+ return ErrorTaskOutput, nil
+ }
+
+ return FullTaskOutput, fmt.Errorf("invalid task output mode: %v", value)
+}
+
+// ToTaskOutputModeString converts a task output mode enum value into the string representation
+func ToTaskOutputModeString(value TaskOutputMode) (string, error) {
+ switch value {
+ case FullTaskOutput:
+ return fullTaskOutputString, nil
+ case NoTaskOutput:
+ return noTaskOutputString, nil
+ case HashTaskOutput:
+ return hashTaskOutputString, nil
+ case NewTaskOutput:
+ return newTaskOutputString, nil
+ case ErrorTaskOutput:
+ return errorTaskOutputString, nil
+ }
+
+ return "", fmt.Errorf("invalid task output mode: %v", value)
+}
+
+// UnmarshalJSON converts a task output mode string representation into an enum
+func (c *TaskOutputMode) UnmarshalJSON(data []byte) error {
+ var rawTaskOutputMode string
+ if err := json.Unmarshal(data, &rawTaskOutputMode); err != nil {
+ return err
+ }
+
+ taskOutputMode, err := FromTaskOutputModeString(rawTaskOutputMode)
+ if err != nil {
+ return err
+ }
+
+ *c = taskOutputMode
+ return nil
+}
+
+// MarshalJSON converts a task output mode to its string representation
+func (c TaskOutputMode) MarshalJSON() ([]byte, error) {
+ outputModeString, err := ToTaskOutputModeString(c)
+ if err != nil {
+ return nil, err
+ }
+ return json.Marshal(outputModeString)
+}