-
Notifications
You must be signed in to change notification settings - Fork 293
/
value.go
147 lines (130 loc) · 4.23 KB
/
value.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
package value
import (
"fmt"
"runtime"
"sort"
"github.com/pkg/errors"
"go.starlark.net/starlark"
"github.com/tilt-dev/tilt/internal/tiltfile/starkit"
"github.com/tilt-dev/tilt/pkg/model"
)
// If `v` is a `starlark.Sequence`, return a slice of its elements
// Otherwise, return it as a single-element slice
// For functions that take `Union[List[T], T]`
func ValueOrSequenceToSlice(v starlark.Value) []starlark.Value {
if seq, ok := v.(starlark.Sequence); ok {
var ret []starlark.Value
it := seq.Iterate()
defer it.Done()
var i starlark.Value
for it.Next(&i) {
ret = append(ret, i)
}
return ret
} else if v == nil || v == starlark.None {
return nil
} else {
return []starlark.Value{v}
}
}
func ValueToAbsPath(thread *starlark.Thread, v starlark.Value) (string, error) {
pathMaker, ok := v.(PathMaker)
if ok {
return pathMaker.MakeLocalPath("."), nil
}
str, ok := starlark.AsString(v)
if ok {
return starkit.AbsPath(thread, string(str)), nil
}
return "", fmt.Errorf("expected path | string. Actual type: %T", v)
}
type PathMaker interface {
MakeLocalPath(relPath string) string
}
func SequenceToStringSlice(seq starlark.Sequence) ([]string, error) {
if seq == nil {
return nil, nil
}
it := seq.Iterate()
defer it.Done()
var ret []string
var v starlark.Value
for it.Next(&v) {
s, ok := v.(starlark.String)
if !ok {
return nil, fmt.Errorf("'%v' is a %T, not a string", v, v)
}
ret = append(ret, string(s))
}
return ret, nil
}
func StringSliceToList(slice []string) *starlark.List {
v := []starlark.Value{}
for _, s := range slice {
v = append(v, starlark.String(s))
}
return starlark.NewList(v)
}
// In other similar build systems (Buck and Bazel),
// there's a "main" command, and then various per-platform overrides.
// https://docs.bazel.build/versions/master/be/general.html#genrule.cmd_bat
// This helper function abstracts out the precedence rules.
func ValueGroupToCmdHelper(t *starlark.Thread, cmdVal, cmdBatVal starlark.Value, env map[string]string) (model.Cmd, error) {
if cmdBatVal != nil && runtime.GOOS == "windows" {
return ValueToBatCmd(t, cmdBatVal, env)
}
return ValueToHostCmd(t, cmdVal, env)
}
// provides dockerfile-style behavior of:
// a string gets interpreted as a shell command (like, sh -c 'foo bar $X')
// an array of strings gets interpreted as a raw argv to exec
func ValueToHostCmd(t *starlark.Thread, v starlark.Value, env map[string]string) (model.Cmd, error) {
return valueToCmdHelper(t, v, env, model.ToHostCmd)
}
func ValueToBatCmd(t *starlark.Thread, v starlark.Value, env map[string]string) (model.Cmd, error) {
return valueToCmdHelper(t, v, env, model.ToBatCmd)
}
func ValueToUnixCmd(t *starlark.Thread, v starlark.Value, env map[string]string) (model.Cmd, error) {
return valueToCmdHelper(t, v, env, model.ToUnixCmd)
}
func valueToCmdHelper(t *starlark.Thread, cmdVal starlark.Value, cmdEnv map[string]string, stringToCmd func(string) model.Cmd) (model.Cmd, error) {
dir := starkit.AbsWorkingDir(t)
env, err := envTuples(cmdEnv)
if err != nil {
return model.Cmd{}, err
}
switch x := cmdVal.(type) {
// If a starlark function takes an optional command argument, then UnpackArgs will set its starlark.Value to nil
// we convert nils here to an empty Cmd, since otherwise every callsite would have to do a nil check with presumably
// the same outcome
case nil:
return model.Cmd{}, nil
case starlark.String:
cmd := stringToCmd(string(x))
cmd.Dir = dir
cmd.Env = env
return cmd, nil
case starlark.Sequence:
argv, err := SequenceToStringSlice(x)
if err != nil {
return model.Cmd{}, errors.Wrap(err, "a command must be a string or a list of strings")
}
return model.Cmd{Argv: argv, Dir: dir, Env: env}, nil
default:
return model.Cmd{}, fmt.Errorf("a command must be a string or list of strings. found %T", x)
}
}
func envTuples(env map[string]string) ([]string, error) {
var kv []string
for k, v := range env {
if k == "" {
return nil, fmt.Errorf("environment has empty key for value %q", v)
}
kv = append(kv, k+"="+v)
}
// sorting here is for consistency/predictability; since the input is a map, uniqueness
// is guaranteed so order is not actually relevant, but this simplifies usage in tests,
// for example
sort.Strings(kv)
return kv, nil
}