blob: b3dd4991f41337d36173a04b4fae8a52b0db691e [file] [log] [blame]
Sasha Smundak24159db2020-10-26 15:43:21 -07001// Copyright 2021 Google LLC
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15package rbcrun
16
17import (
18 "fmt"
19 "os"
20 "os/exec"
21 "path/filepath"
22 "regexp"
23 "strings"
24
25 "go.starlark.net/starlark"
26 "go.starlark.net/starlarkstruct"
27)
28
29const callerDirKey = "callerDir"
30
31var LoadPathRoot = "."
32var shellPath string
33
34type modentry struct {
35 globals starlark.StringDict
36 err error
37}
38
39var moduleCache = make(map[string]*modentry)
40
41var builtins starlark.StringDict
42
43func moduleName2AbsPath(moduleName string, callerDir string) (string, error) {
44 path := moduleName
45 if ix := strings.LastIndex(path, ":"); ix >= 0 {
46 path = path[0:ix] + string(os.PathSeparator) + path[ix+1:]
47 }
48 if strings.HasPrefix(path, "//") {
49 return filepath.Abs(filepath.Join(LoadPathRoot, path[2:]))
50 } else if strings.HasPrefix(moduleName, ":") {
51 return filepath.Abs(filepath.Join(callerDir, path[1:]))
52 } else {
53 return filepath.Abs(path)
54 }
55}
56
57// loader implements load statement. The format of the loaded module URI is
58// [//path]:base[|symbol]
59// The file path is $ROOT/path/base if path is present, <caller_dir>/base otherwise.
60// The presence of `|symbol` indicates that the loader should return a single 'symbol'
61// bound to None if file is missing.
62func loader(thread *starlark.Thread, module string) (starlark.StringDict, error) {
63 pipePos := strings.LastIndex(module, "|")
64 mustLoad := pipePos < 0
65 var defaultSymbol string
66 if !mustLoad {
67 defaultSymbol = module[pipePos+1:]
68 module = module[:pipePos]
69 }
70 modulePath, err := moduleName2AbsPath(module, thread.Local(callerDirKey).(string))
71 if err != nil {
72 return nil, err
73 }
74 e, ok := moduleCache[modulePath]
75 if e == nil {
76 if ok {
77 return nil, fmt.Errorf("cycle in load graph")
78 }
79
80 // Add a placeholder to indicate "load in progress".
81 moduleCache[modulePath] = nil
82
83 // Decide if we should load.
84 if !mustLoad {
85 if _, err := os.Stat(modulePath); err == nil {
86 mustLoad = true
87 }
88 }
89
90 // Load or return default
91 if mustLoad {
92 childThread := &starlark.Thread{Name: "exec " + module, Load: thread.Load}
93 // Cheating for the sake of testing:
94 // propagate starlarktest's Reporter key, otherwise testing
95 // the load function may cause panic in starlarktest code.
96 const testReporterKey = "Reporter"
97 if v := thread.Local(testReporterKey); v != nil {
98 childThread.SetLocal(testReporterKey, v)
99 }
100
101 childThread.SetLocal(callerDirKey, filepath.Dir(modulePath))
102 globals, err := starlark.ExecFile(childThread, modulePath, nil, builtins)
103 e = &modentry{globals, err}
104 } else {
105 e = &modentry{starlark.StringDict{defaultSymbol: starlark.None}, nil}
106 }
107
108 // Update the cache.
109 moduleCache[modulePath] = e
110 }
111 return e.globals, e.err
112}
113
114// fileExists returns True if file with given name exists.
115func fileExists(_ *starlark.Thread, b *starlark.Builtin, args starlark.Tuple,
116 kwargs []starlark.Tuple) (starlark.Value, error) {
117 var path string
118 if err := starlark.UnpackPositionalArgs(b.Name(), args, kwargs, 1, &path); err != nil {
119 return starlark.None, err
120 }
Sasha Smundak3c569792021-08-05 17:33:15 -0700121 if _, err := os.Stat(path); err != nil {
Sasha Smundak24159db2020-10-26 15:43:21 -0700122 return starlark.False, nil
123 }
124 return starlark.True, nil
125}
126
127// regexMatch(pattern, s) returns True if s matches pattern (a regex)
128func regexMatch(_ *starlark.Thread, b *starlark.Builtin, args starlark.Tuple,
129 kwargs []starlark.Tuple) (starlark.Value, error) {
130 var pattern, s string
131 if err := starlark.UnpackPositionalArgs(b.Name(), args, kwargs, 2, &pattern, &s); err != nil {
132 return starlark.None, err
133 }
134 match, err := regexp.MatchString(pattern, s)
135 if err != nil {
136 return starlark.None, err
137 }
138 if match {
139 return starlark.True, nil
140 }
141 return starlark.False, nil
142}
143
144// wildcard(pattern, top=None) expands shell's glob pattern. If 'top' is present,
145// the 'top/pattern' is globbed and then 'top/' prefix is removed.
146func wildcard(_ *starlark.Thread, b *starlark.Builtin, args starlark.Tuple,
147 kwargs []starlark.Tuple) (starlark.Value, error) {
148 var pattern string
149 var top string
150
151 if err := starlark.UnpackPositionalArgs(b.Name(), args, kwargs, 1, &pattern, &top); err != nil {
152 return starlark.None, err
153 }
154
155 var files []string
156 var err error
157 if top == "" {
158 if files, err = filepath.Glob(pattern); err != nil {
159 return starlark.None, err
160 }
161 } else {
162 prefix := top + string(filepath.Separator)
163 if files, err = filepath.Glob(prefix + pattern); err != nil {
164 return starlark.None, err
165 }
166 for i := range files {
167 files[i] = strings.TrimPrefix(files[i], prefix)
168 }
169 }
170 return makeStringList(files), nil
171}
172
173// shell(command) runs OS shell with given command and returns back
174// its output the same way as Make's $(shell ) function. The end-of-lines
175// ("\n" or "\r\n") are replaced with " " in the result, and the trailing
176// end-of-line is removed.
177func shell(_ *starlark.Thread, b *starlark.Builtin, args starlark.Tuple,
178 kwargs []starlark.Tuple) (starlark.Value, error) {
179 var command string
180 if err := starlark.UnpackPositionalArgs(b.Name(), args, kwargs, 1, &command); err != nil {
181 return starlark.None, err
182 }
183 if shellPath == "" {
184 return starlark.None,
Sasha Smundak57bb5082021-04-01 15:51:56 -0700185 fmt.Errorf("cannot run shell, /bin/sh is missing (running on Windows?)")
Sasha Smundak24159db2020-10-26 15:43:21 -0700186 }
187 cmd := exec.Command(shellPath, "-c", command)
188 // We ignore command's status
189 bytes, _ := cmd.Output()
190 output := string(bytes)
191 if strings.HasSuffix(output, "\n") {
192 output = strings.TrimSuffix(output, "\n")
193 } else {
194 output = strings.TrimSuffix(output, "\r\n")
195 }
196
197 return starlark.String(
198 strings.ReplaceAll(
199 strings.ReplaceAll(output, "\r\n", " "),
200 "\n", " ")), nil
201}
202
203func makeStringList(items []string) *starlark.List {
204 elems := make([]starlark.Value, len(items))
205 for i, item := range items {
206 elems[i] = starlark.String(item)
207 }
208 return starlark.NewList(elems)
209}
210
211// propsetFromEnv constructs a propset from the array of KEY=value strings
212func structFromEnv(env []string) *starlarkstruct.Struct {
213 sd := make(map[string]starlark.Value, len(env))
214 for _, x := range env {
215 kv := strings.SplitN(x, "=", 2)
216 sd[kv[0]] = starlark.String(kv[1])
217 }
218 return starlarkstruct.FromStringDict(starlarkstruct.Default, sd)
219}
220
221func setup(env []string) {
222 // Create the symbols that aid makefile conversion. See README.md
223 builtins = starlark.StringDict{
224 "struct": starlark.NewBuiltin("struct", starlarkstruct.Make),
225 "rblf_cli": structFromEnv(env),
226 "rblf_env": structFromEnv(os.Environ()),
227 // To convert makefile's $(wildcard foo)
228 "rblf_file_exists": starlark.NewBuiltin("rblf_file_exists", fileExists),
229 // To convert makefile's $(filter ...)/$(filter-out)
230 "rblf_regex": starlark.NewBuiltin("rblf_regex", regexMatch),
231 // To convert makefile's $(shell cmd)
232 "rblf_shell": starlark.NewBuiltin("rblf_shell", shell),
233 // To convert makefile's $(wildcard foo*)
234 "rblf_wildcard": starlark.NewBuiltin("rblf_wildcard", wildcard),
235 }
236
Sasha Smundak57bb5082021-04-01 15:51:56 -0700237 // NOTE(asmundak): OS-specific. Behave similar to Linux `system` call,
238 // which always uses /bin/sh to run the command
239 shellPath = "/bin/sh"
240 if _, err := os.Stat(shellPath); err != nil {
241 shellPath = ""
242 }
Sasha Smundak24159db2020-10-26 15:43:21 -0700243}
244
245// Parses, resolves, and executes a Starlark file.
246// filename and src parameters are as for starlark.ExecFile:
247// * filename is the name of the file to execute,
248// and the name that appears in error messages;
249// * src is an optional source of bytes to use instead of filename
250// (it can be a string, or a byte array, or an io.Reader instance)
251// * commandVars is an array of "VAR=value" items. They are accessible from
252// the starlark script as members of the `rblf_cli` propset.
253func Run(filename string, src interface{}, commandVars []string) error {
254 setup(commandVars)
255
256 mainThread := &starlark.Thread{
257 Name: "main",
258 Print: func(_ *starlark.Thread, msg string) { fmt.Println(msg) },
259 Load: loader,
260 }
261 absPath, err := filepath.Abs(filename)
262 if err == nil {
263 mainThread.SetLocal(callerDirKey, filepath.Dir(absPath))
264 _, err = starlark.ExecFile(mainThread, absPath, src, builtins)
265 }
266 return err
267}