blob: c6e89f02c836486b79e2f2fd0eda986e606569bd [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"
Sasha Smundak6b795dc2021-08-18 16:32:19 -070019 "io/fs"
Sasha Smundak24159db2020-10-26 15:43:21 -070020 "os"
21 "os/exec"
22 "path/filepath"
Sasha Smundak24159db2020-10-26 15:43:21 -070023 "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
Sasha Smundak24159db2020-10-26 15:43:21 -0700127// wildcard(pattern, top=None) expands shell's glob pattern. If 'top' is present,
128// the 'top/pattern' is globbed and then 'top/' prefix is removed.
129func wildcard(_ *starlark.Thread, b *starlark.Builtin, args starlark.Tuple,
130 kwargs []starlark.Tuple) (starlark.Value, error) {
131 var pattern string
132 var top string
133
134 if err := starlark.UnpackPositionalArgs(b.Name(), args, kwargs, 1, &pattern, &top); err != nil {
135 return starlark.None, err
136 }
137
138 var files []string
139 var err error
140 if top == "" {
141 if files, err = filepath.Glob(pattern); err != nil {
142 return starlark.None, err
143 }
144 } else {
145 prefix := top + string(filepath.Separator)
146 if files, err = filepath.Glob(prefix + pattern); err != nil {
147 return starlark.None, err
148 }
149 for i := range files {
150 files[i] = strings.TrimPrefix(files[i], prefix)
151 }
152 }
153 return makeStringList(files), nil
154}
155
Sasha Smundak6b795dc2021-08-18 16:32:19 -0700156// find(top, pattern, only_files = 0) returns all the paths under 'top'
157// whose basename matches 'pattern' (which is a shell's glob pattern).
158// If 'only_files' is non-zero, only the paths to the regular files are
159// returned. The returned paths are relative to 'top'.
160func find(_ *starlark.Thread, b *starlark.Builtin, args starlark.Tuple,
161 kwargs []starlark.Tuple) (starlark.Value, error) {
162 var top, pattern string
163 var onlyFiles int
164 if err := starlark.UnpackArgs(b.Name(), args, kwargs,
165 "top", &top, "pattern", &pattern, "only_files?", &onlyFiles); err != nil {
166 return starlark.None, err
167 }
168 top = filepath.Clean(top)
169 pattern = filepath.Clean(pattern)
170 // Go's filepath.Walk is slow, consider using OS's find
171 var res []string
172 err := filepath.WalkDir(top, func(path string, d fs.DirEntry, err error) error {
173 if err != nil {
174 if d != nil && d.IsDir() {
175 return fs.SkipDir
176 } else {
177 return nil
178 }
179 }
180 relPath := strings.TrimPrefix(path, top)
181 if len(relPath) > 0 && relPath[0] == os.PathSeparator {
182 relPath = relPath[1:]
183 }
184 // Do not return top-level dir
185 if len(relPath) == 0 {
186 return nil
187 }
188 if matched, err := filepath.Match(pattern, d.Name()); err == nil && matched && (onlyFiles == 0 || d.Type().IsRegular()) {
189 res = append(res, relPath)
190 }
191 return nil
192 })
193 return makeStringList(res), err
194}
195
Sasha Smundak24159db2020-10-26 15:43:21 -0700196// shell(command) runs OS shell with given command and returns back
197// its output the same way as Make's $(shell ) function. The end-of-lines
198// ("\n" or "\r\n") are replaced with " " in the result, and the trailing
199// end-of-line is removed.
200func shell(_ *starlark.Thread, b *starlark.Builtin, args starlark.Tuple,
201 kwargs []starlark.Tuple) (starlark.Value, error) {
202 var command string
203 if err := starlark.UnpackPositionalArgs(b.Name(), args, kwargs, 1, &command); err != nil {
204 return starlark.None, err
205 }
206 if shellPath == "" {
207 return starlark.None,
Sasha Smundak57bb5082021-04-01 15:51:56 -0700208 fmt.Errorf("cannot run shell, /bin/sh is missing (running on Windows?)")
Sasha Smundak24159db2020-10-26 15:43:21 -0700209 }
210 cmd := exec.Command(shellPath, "-c", command)
211 // We ignore command's status
212 bytes, _ := cmd.Output()
213 output := string(bytes)
214 if strings.HasSuffix(output, "\n") {
215 output = strings.TrimSuffix(output, "\n")
216 } else {
217 output = strings.TrimSuffix(output, "\r\n")
218 }
219
220 return starlark.String(
221 strings.ReplaceAll(
222 strings.ReplaceAll(output, "\r\n", " "),
223 "\n", " ")), nil
224}
225
226func makeStringList(items []string) *starlark.List {
227 elems := make([]starlark.Value, len(items))
228 for i, item := range items {
229 elems[i] = starlark.String(item)
230 }
231 return starlark.NewList(elems)
232}
233
234// propsetFromEnv constructs a propset from the array of KEY=value strings
235func structFromEnv(env []string) *starlarkstruct.Struct {
236 sd := make(map[string]starlark.Value, len(env))
237 for _, x := range env {
238 kv := strings.SplitN(x, "=", 2)
239 sd[kv[0]] = starlark.String(kv[1])
240 }
241 return starlarkstruct.FromStringDict(starlarkstruct.Default, sd)
242}
243
Sasha Smundake8652d42021-09-24 08:25:17 -0700244func log(thread *starlark.Thread, fn *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) {
245 sep := " "
246 if err := starlark.UnpackArgs("print", nil, kwargs, "sep?", &sep); err != nil {
247 return nil, err
248 }
249 for i, v := range args {
250 if i > 0 {
251 fmt.Fprint(os.Stderr, sep)
252 }
253 if s, ok := starlark.AsString(v); ok {
254 fmt.Fprint(os.Stderr, s)
255 } else if b, ok := v.(starlark.Bytes); ok {
256 fmt.Fprint(os.Stderr, string(b))
257 } else {
258 fmt.Fprintf(os.Stderr, "%s", v)
259 }
260 }
261
262 fmt.Fprintln(os.Stderr)
263 return starlark.None, nil
264}
265
Sasha Smundak24159db2020-10-26 15:43:21 -0700266func setup(env []string) {
267 // Create the symbols that aid makefile conversion. See README.md
268 builtins = starlark.StringDict{
269 "struct": starlark.NewBuiltin("struct", starlarkstruct.Make),
270 "rblf_cli": structFromEnv(env),
271 "rblf_env": structFromEnv(os.Environ()),
272 // To convert makefile's $(wildcard foo)
273 "rblf_file_exists": starlark.NewBuiltin("rblf_file_exists", fileExists),
Sasha Smundak6b795dc2021-08-18 16:32:19 -0700274 // To convert find-copy-subdir and product-copy-files-by pattern
275 "rblf_find_files": starlark.NewBuiltin("rblf_find_files", find),
Sasha Smundak24159db2020-10-26 15:43:21 -0700276 // To convert makefile's $(shell cmd)
277 "rblf_shell": starlark.NewBuiltin("rblf_shell", shell),
Sasha Smundake8652d42021-09-24 08:25:17 -0700278 // Output to stderr
279 "rblf_log": starlark.NewBuiltin("rblf_log", log),
Sasha Smundak24159db2020-10-26 15:43:21 -0700280 // To convert makefile's $(wildcard foo*)
281 "rblf_wildcard": starlark.NewBuiltin("rblf_wildcard", wildcard),
282 }
283
Sasha Smundak57bb5082021-04-01 15:51:56 -0700284 // NOTE(asmundak): OS-specific. Behave similar to Linux `system` call,
285 // which always uses /bin/sh to run the command
286 shellPath = "/bin/sh"
287 if _, err := os.Stat(shellPath); err != nil {
288 shellPath = ""
289 }
Sasha Smundak24159db2020-10-26 15:43:21 -0700290}
291
292// Parses, resolves, and executes a Starlark file.
293// filename and src parameters are as for starlark.ExecFile:
294// * filename is the name of the file to execute,
295// and the name that appears in error messages;
296// * src is an optional source of bytes to use instead of filename
297// (it can be a string, or a byte array, or an io.Reader instance)
298// * commandVars is an array of "VAR=value" items. They are accessible from
299// the starlark script as members of the `rblf_cli` propset.
300func Run(filename string, src interface{}, commandVars []string) error {
301 setup(commandVars)
302
303 mainThread := &starlark.Thread{
304 Name: "main",
305 Print: func(_ *starlark.Thread, msg string) { fmt.Println(msg) },
306 Load: loader,
307 }
308 absPath, err := filepath.Abs(filename)
309 if err == nil {
310 mainThread.SetLocal(callerDirKey, filepath.Dir(absPath))
311 _, err = starlark.ExecFile(mainThread, absPath, src, builtins)
312 }
313 return err
314}