github.com/tilt-dev/tilt@v0.33.15-0.20240515162809-0a22ed45d8a0/internal/tiltfile/value/value.go (about)

     1  package value
     2  
     3  import (
     4  	"fmt"
     5  	"math"
     6  	"runtime"
     7  	"sort"
     8  
     9  	"github.com/pkg/errors"
    10  	"go.starlark.net/starlark"
    11  
    12  	"github.com/tilt-dev/tilt/internal/tiltfile/starkit"
    13  	"github.com/tilt-dev/tilt/pkg/model"
    14  )
    15  
    16  // If `v` is a `starlark.Sequence`, return a slice of its elements
    17  // Otherwise, return it as a single-element slice
    18  // For functions that take `Union[List[T], T]`
    19  func ValueOrSequenceToSlice(v starlark.Value) []starlark.Value {
    20  	if seq, ok := v.(starlark.Sequence); ok {
    21  		var ret []starlark.Value
    22  		it := seq.Iterate()
    23  		defer it.Done()
    24  		var i starlark.Value
    25  		for it.Next(&i) {
    26  			ret = append(ret, i)
    27  		}
    28  		return ret
    29  	} else if v == nil || v == starlark.None {
    30  		return nil
    31  	} else {
    32  		return []starlark.Value{v}
    33  	}
    34  }
    35  
    36  func ValueToAbsPath(thread *starlark.Thread, v starlark.Value) (string, error) {
    37  	pathMaker, ok := v.(PathMaker)
    38  	if ok {
    39  		return pathMaker.MakeLocalPath("."), nil
    40  	}
    41  
    42  	str, ok := starlark.AsString(v)
    43  	if ok {
    44  		return starkit.AbsPath(thread, str), nil
    45  	}
    46  
    47  	return "", fmt.Errorf("expected path | string. Actual type: %T", v)
    48  }
    49  
    50  type PathMaker interface {
    51  	MakeLocalPath(relPath string) string
    52  }
    53  
    54  // StringSequence is a convenience type for dealing with string slices in Starlark.
    55  type StringSequence []string
    56  
    57  func (s StringSequence) Sequence() starlark.Sequence {
    58  	elems := make([]starlark.Value, 0, len(s))
    59  	for _, v := range s {
    60  		elems = append(elems, starlark.String(v))
    61  	}
    62  	return starlark.NewList(elems)
    63  }
    64  
    65  func (s *StringSequence) Unpack(v starlark.Value) error {
    66  	if v == nil {
    67  		*s = nil
    68  		return nil
    69  	}
    70  	seq, ok := v.(starlark.Sequence)
    71  	if !ok {
    72  		return fmt.Errorf("'%v' is a %T, not a sequence", v, v)
    73  	}
    74  	out, err := SequenceToStringSlice(seq)
    75  	if err != nil {
    76  		return err
    77  	}
    78  	*s = out
    79  	return nil
    80  }
    81  
    82  func SequenceToStringSlice(seq starlark.Sequence) ([]string, error) {
    83  	if seq == nil || seq.Len() == 0 {
    84  		return nil, nil
    85  	}
    86  	it := seq.Iterate()
    87  	defer it.Done()
    88  	ret := make([]string, 0, seq.Len())
    89  	var v starlark.Value
    90  	for it.Next(&v) {
    91  		s, ok := v.(starlark.String)
    92  		if !ok {
    93  			return nil, fmt.Errorf("'%v' is a %T, not a string", v, v)
    94  		}
    95  		ret = append(ret, string(s))
    96  	}
    97  	return ret, nil
    98  }
    99  
   100  func StringSliceToList(slice []string) *starlark.List {
   101  	v := []starlark.Value{}
   102  	for _, s := range slice {
   103  		v = append(v, starlark.String(s))
   104  	}
   105  	return starlark.NewList(v)
   106  }
   107  
   108  // In other similar build systems (Buck and Bazel),
   109  // there's a "main" command, and then various per-platform overrides.
   110  // https://docs.bazel.build/versions/master/be/general.html#genrule.cmd_bat
   111  // This helper function abstracts out the precedence rules.
   112  func ValueGroupToCmdHelper(t *starlark.Thread, cmdVal, cmdBatVal, cmdDir starlark.Value, env map[string]string) (model.Cmd, error) {
   113  	if cmdBatVal != nil && runtime.GOOS == "windows" {
   114  		return ValueToBatCmd(t, cmdBatVal, cmdDir, env)
   115  	}
   116  	return ValueToHostCmd(t, cmdVal, cmdDir, env)
   117  }
   118  
   119  // provides dockerfile-style behavior of:
   120  // a string gets interpreted as a shell command (like, sh -c 'foo bar $X')
   121  // an array of strings gets interpreted as a raw argv to exec
   122  func ValueToHostCmd(t *starlark.Thread, v, dir starlark.Value, env map[string]string) (model.Cmd, error) {
   123  	return valueToCmdHelper(t, v, dir, env, model.ToHostCmd)
   124  }
   125  
   126  func ValueToBatCmd(t *starlark.Thread, v, dir starlark.Value, env map[string]string) (model.Cmd, error) {
   127  	return valueToCmdHelper(t, v, dir, env, model.ToBatCmd)
   128  }
   129  
   130  func ValueToUnixCmd(t *starlark.Thread, v, dir starlark.Value, env map[string]string) (model.Cmd, error) {
   131  	return valueToCmdHelper(t, v, dir, env, model.ToUnixCmd)
   132  }
   133  
   134  func valueToCmdHelper(t *starlark.Thread, cmdVal, cmdDirVal starlark.Value, cmdEnv map[string]string, stringToCmd func(string) model.Cmd) (model.Cmd, error) {
   135  
   136  	var dir string
   137  	var dirErr error
   138  
   139  	switch cmdDirVal.(type) {
   140  	case nil:
   141  		dir = starkit.AbsWorkingDir(t)
   142  	case starlark.NoneType:
   143  		dir = starkit.AbsWorkingDir(t)
   144  	default:
   145  		dir, dirErr = ValueToAbsPath(t, cmdDirVal)
   146  		if dirErr != nil {
   147  			return model.Cmd{}, errors.Wrap(dirErr, "a command directory must be empty or a string")
   148  		}
   149  	}
   150  
   151  	env, err := envTuples(cmdEnv)
   152  	if err != nil {
   153  		return model.Cmd{}, err
   154  	}
   155  
   156  	switch x := cmdVal.(type) {
   157  	// If a starlark function takes an optional command argument, then UnpackArgs will set its starlark.Value to nil
   158  	// we convert nils here to an empty Cmd, since otherwise every callsite would have to do a nil check with presumably
   159  	// the same outcome
   160  	case nil:
   161  		return model.Cmd{}, nil
   162  	case starlark.String:
   163  		cmd := stringToCmd(string(x))
   164  		cmd.Dir = dir
   165  		cmd.Env = env
   166  		return cmd, nil
   167  	case starlark.Sequence:
   168  		argv, err := SequenceToStringSlice(x)
   169  		if err != nil {
   170  			return model.Cmd{}, errors.Wrap(err, "a command must be a string or a list of strings")
   171  		}
   172  		return model.Cmd{Argv: argv, Dir: dir, Env: env}, nil
   173  	default:
   174  		return model.Cmd{}, fmt.Errorf("a command must be a string or list of strings. found %T", x)
   175  	}
   176  }
   177  
   178  func envTuples(env map[string]string) ([]string, error) {
   179  	var kv []string
   180  	for k, v := range env {
   181  		if k == "" {
   182  			return nil, fmt.Errorf("environment has empty key for value %q", v)
   183  		}
   184  		kv = append(kv, k+"="+v)
   185  	}
   186  	// sorting here is for consistency/predictability; since the input is a map, uniqueness
   187  	// is guaranteed so order is not actually relevant, but this simplifies usage in tests,
   188  	// for example
   189  	sort.Strings(kv)
   190  	return kv, nil
   191  }
   192  
   193  // Int32 is a convenience type for unpacking int32 bounded values.
   194  type Int32 struct {
   195  	starlark.Int
   196  }
   197  
   198  // Int32 returns the value as an int32.
   199  //
   200  // It will panic if the value cannot be accurate represented as an int32.
   201  func (i Int32) Int32() int32 {
   202  	v, err := starlarkIntAsInt32(i.Int)
   203  	if err != nil {
   204  		// bounds check should have happened during unpacking, so something
   205  		// is very wrong if we get here
   206  		panic(err)
   207  	}
   208  	return v
   209  }
   210  
   211  func (i *Int32) Unpack(v starlark.Value) error {
   212  	if v == nil {
   213  		return fmt.Errorf("got %s, want int", starlark.None.Type())
   214  	}
   215  	x, ok := v.(starlark.Int)
   216  	if !ok {
   217  		return fmt.Errorf("got %s, want int", v.Type())
   218  	}
   219  	if _, err := starlarkIntAsInt32(x); err != nil {
   220  		return err
   221  	}
   222  	i.Int = x
   223  	return nil
   224  }
   225  
   226  func starlarkIntAsInt32(v starlark.Int) (int32, error) {
   227  	x, ok := v.Int64()
   228  	if !ok || x < math.MinInt32 || x > math.MaxInt32 {
   229  		return 0, fmt.Errorf("value out of range for int32: %s", v.String())
   230  	}
   231  	return int32(x), nil
   232  }