blob: 7f4f72dac668029ff5a8234712e5962b89558896 [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"
23 "regexp"
24 "strings"
25
26 "go.starlark.net/starlark"
27 "go.starlark.net/starlarkstruct"
28)
29
30const callerDirKey = "callerDir"
31
32var LoadPathRoot = "."
33var shellPath string
34
35type modentry struct {
36 globals starlark.StringDict
37 err error
38}
39
40var moduleCache = make(map[string]*modentry)
41
42var builtins starlark.StringDict
43
44func moduleName2AbsPath(moduleName string, callerDir string) (string, error) {
45 path := moduleName
46 if ix := strings.LastIndex(path, ":"); ix >= 0 {
47 path = path[0:ix] + string(os.PathSeparator) + path[ix+1:]
48 }
49 if strings.HasPrefix(path, "//") {
50 return filepath.Abs(filepath.Join(LoadPathRoot, path[2:]))
51 } else if strings.HasPrefix(moduleName, ":") {
52 return filepath.Abs(filepath.Join(callerDir, path[1:]))
53 } else {
54 return filepath.Abs(path)
55 }
56}
57
58// loader implements load statement. The format of the loaded module URI is
59// [//path]:base[|symbol]
60// The file path is $ROOT/path/base if path is present, <caller_dir>/base otherwise.
61// The presence of `|symbol` indicates that the loader should return a single 'symbol'
62// bound to None if file is missing.
63func loader(thread *starlark.Thread, module string) (starlark.StringDict, error) {
64 pipePos := strings.LastIndex(module, "|")
65 mustLoad := pipePos < 0
66 var defaultSymbol string
67 if !mustLoad {
68 defaultSymbol = module[pipePos+1:]
69 module = module[:pipePos]
70 }
71 modulePath, err := moduleName2AbsPath(module, thread.Local(callerDirKey).(string))
72 if err != nil {
73 return nil, err
74 }
75 e, ok := moduleCache[modulePath]
76 if e == nil {
77 if ok {
78 return nil, fmt.Errorf("cycle in load graph")
79 }
80
81 // Add a placeholder to indicate "load in progress".
82 moduleCache[modulePath] = nil
83
84 // Decide if we should load.
85 if !mustLoad {
86 if _, err := os.Stat(modulePath); err == nil {
87 mustLoad = true
88 }
89 }
90
91 // Load or return default
92 if mustLoad {
93 childThread := &starlark.Thread{Name: "exec " + module, Load: thread.Load}
94 // Cheating for the sake of testing:
95 // propagate starlarktest's Reporter key, otherwise testing
96 // the load function may cause panic in starlarktest code.
97 const testReporterKey = "Reporter"
98 if v := thread.Local(testReporterKey); v != nil {
99 childThread.SetLocal(testReporterKey, v)
100 }
101
102 childThread.SetLocal(callerDirKey, filepath.Dir(modulePath))
103 globals, err := starlark.ExecFile(childThread, modulePath, nil, builtins)
104 e = &modentry{globals, err}
105 } else {
106 e = &modentry{starlark.StringDict{defaultSymbol: starlark.None}, nil}
107 }
108
109 // Update the cache.
110 moduleCache[modulePath] = e
111 }
112 return e.globals, e.err
113}
114
115// fileExists returns True if file with given name exists.
116func fileExists(_ *starlark.Thread, b *starlark.Builtin, args starlark.Tuple,
117 kwargs []starlark.Tuple) (starlark.Value, error) {
118 var path string
119 if err := starlark.UnpackPositionalArgs(b.Name(), args, kwargs, 1, &path); err != nil {
120 return starlark.None, err
121 }
Sasha Smundak3c569792021-08-05 17:33:15 -0700122 if _, err := os.Stat(path); err != nil {
Sasha Smundak24159db2020-10-26 15:43:21 -0700123 return starlark.False, nil
124 }
125 return starlark.True, nil
126}
127
128// regexMatch(pattern, s) returns True if s matches pattern (a regex)
129func regexMatch(_ *starlark.Thread, b *starlark.Builtin, args starlark.Tuple,
130 kwargs []starlark.Tuple) (starlark.Value, error) {
131 var pattern, s string
132 if err := starlark.UnpackPositionalArgs(b.Name(), args, kwargs, 2, &pattern, &s); err != nil {
133 return starlark.None, err
134 }
135 match, err := regexp.MatchString(pattern, s)
136 if err != nil {
137 return starlark.None, err
138 }
139 if match {
140 return starlark.True, nil
141 }
142 return starlark.False, nil
143}
144
145// wildcard(pattern, top=None) expands shell's glob pattern. If 'top' is present,
146// the 'top/pattern' is globbed and then 'top/' prefix is removed.
147func wildcard(_ *starlark.Thread, b *starlark.Builtin, args starlark.Tuple,
148 kwargs []starlark.Tuple) (starlark.Value, error) {
149 var pattern string
150 var top string
151
152 if err := starlark.UnpackPositionalArgs(b.Name(), args, kwargs, 1, &pattern, &top); err != nil {
153 return starlark.None, err
154 }
155
156 var files []string
157 var err error
158 if top == "" {
159 if files, err = filepath.Glob(pattern); err != nil {
160 return starlark.None, err
161 }
162 } else {
163 prefix := top + string(filepath.Separator)
164 if files, err = filepath.Glob(prefix + pattern); err != nil {
165 return starlark.None, err
166 }
167 for i := range files {
168 files[i] = strings.TrimPrefix(files[i], prefix)
169 }
170 }
171 return makeStringList(files), nil
172}
173
Sasha Smundak6b795dc2021-08-18 16:32:19 -0700174// find(top, pattern, only_files = 0) returns all the paths under 'top'
175// whose basename matches 'pattern' (which is a shell's glob pattern).
176// If 'only_files' is non-zero, only the paths to the regular files are
177// returned. The returned paths are relative to 'top'.
178func find(_ *starlark.Thread, b *starlark.Builtin, args starlark.Tuple,
179 kwargs []starlark.Tuple) (starlark.Value, error) {
180 var top, pattern string
181 var onlyFiles int
182 if err := starlark.UnpackArgs(b.Name(), args, kwargs,
183 "top", &top, "pattern", &pattern, "only_files?", &onlyFiles); err != nil {
184 return starlark.None, err
185 }
186 top = filepath.Clean(top)
187 pattern = filepath.Clean(pattern)
188 // Go's filepath.Walk is slow, consider using OS's find
189 var res []string
190 err := filepath.WalkDir(top, func(path string, d fs.DirEntry, err error) error {
191 if err != nil {
192 if d != nil && d.IsDir() {
193 return fs.SkipDir
194 } else {
195 return nil
196 }
197 }
198 relPath := strings.TrimPrefix(path, top)
199 if len(relPath) > 0 && relPath[0] == os.PathSeparator {
200 relPath = relPath[1:]
201 }
202 // Do not return top-level dir
203 if len(relPath) == 0 {
204 return nil
205 }
206 if matched, err := filepath.Match(pattern, d.Name()); err == nil && matched && (onlyFiles == 0 || d.Type().IsRegular()) {
207 res = append(res, relPath)
208 }
209 return nil
210 })
211 return makeStringList(res), err
212}
213
Sasha Smundak24159db2020-10-26 15:43:21 -0700214// shell(command) runs OS shell with given command and returns back
215// its output the same way as Make's $(shell ) function. The end-of-lines
216// ("\n" or "\r\n") are replaced with " " in the result, and the trailing
217// end-of-line is removed.
218func shell(_ *starlark.Thread, b *starlark.Builtin, args starlark.Tuple,
219 kwargs []starlark.Tuple) (starlark.Value, error) {
220 var command string
221 if err := starlark.UnpackPositionalArgs(b.Name(), args, kwargs, 1, &command); err != nil {
222 return starlark.None, err
223 }
224 if shellPath == "" {
225 return starlark.None,
Sasha Smundak57bb5082021-04-01 15:51:56 -0700226 fmt.Errorf("cannot run shell, /bin/sh is missing (running on Windows?)")
Sasha Smundak24159db2020-10-26 15:43:21 -0700227 }
228 cmd := exec.Command(shellPath, "-c", command)
229 // We ignore command's status
230 bytes, _ := cmd.Output()
231 output := string(bytes)
232 if strings.HasSuffix(output, "\n") {
233 output = strings.TrimSuffix(output, "\n")
234 } else {
235 output = strings.TrimSuffix(output, "\r\n")
236 }
237
238 return starlark.String(
239 strings.ReplaceAll(
240 strings.ReplaceAll(output, "\r\n", " "),
241 "\n", " ")), nil
242}
243
244func makeStringList(items []string) *starlark.List {
245 elems := make([]starlark.Value, len(items))
246 for i, item := range items {
247 elems[i] = starlark.String(item)
248 }
249 return starlark.NewList(elems)
250}
251
252// propsetFromEnv constructs a propset from the array of KEY=value strings
253func structFromEnv(env []string) *starlarkstruct.Struct {
254 sd := make(map[string]starlark.Value, len(env))
255 for _, x := range env {
256 kv := strings.SplitN(x, "=", 2)
257 sd[kv[0]] = starlark.String(kv[1])
258 }
259 return starlarkstruct.FromStringDict(starlarkstruct.Default, sd)
260}
261
262func setup(env []string) {
263 // Create the symbols that aid makefile conversion. See README.md
264 builtins = starlark.StringDict{
265 "struct": starlark.NewBuiltin("struct", starlarkstruct.Make),
266 "rblf_cli": structFromEnv(env),
267 "rblf_env": structFromEnv(os.Environ()),
268 // To convert makefile's $(wildcard foo)
269 "rblf_file_exists": starlark.NewBuiltin("rblf_file_exists", fileExists),
Sasha Smundak6b795dc2021-08-18 16:32:19 -0700270 // To convert find-copy-subdir and product-copy-files-by pattern
271 "rblf_find_files": starlark.NewBuiltin("rblf_find_files", find),
Sasha Smundak24159db2020-10-26 15:43:21 -0700272 // To convert makefile's $(filter ...)/$(filter-out)
273 "rblf_regex": starlark.NewBuiltin("rblf_regex", regexMatch),
274 // To convert makefile's $(shell cmd)
275 "rblf_shell": starlark.NewBuiltin("rblf_shell", shell),
276 // To convert makefile's $(wildcard foo*)
277 "rblf_wildcard": starlark.NewBuiltin("rblf_wildcard", wildcard),
278 }
279
Sasha Smundak57bb5082021-04-01 15:51:56 -0700280 // NOTE(asmundak): OS-specific. Behave similar to Linux `system` call,
281 // which always uses /bin/sh to run the command
282 shellPath = "/bin/sh"
283 if _, err := os.Stat(shellPath); err != nil {
284 shellPath = ""
285 }
Sasha Smundak24159db2020-10-26 15:43:21 -0700286}
287
288// Parses, resolves, and executes a Starlark file.
289// filename and src parameters are as for starlark.ExecFile:
290// * filename is the name of the file to execute,
291// and the name that appears in error messages;
292// * src is an optional source of bytes to use instead of filename
293// (it can be a string, or a byte array, or an io.Reader instance)
294// * commandVars is an array of "VAR=value" items. They are accessible from
295// the starlark script as members of the `rblf_cli` propset.
296func Run(filename string, src interface{}, commandVars []string) error {
297 setup(commandVars)
298
299 mainThread := &starlark.Thread{
300 Name: "main",
301 Print: func(_ *starlark.Thread, msg string) { fmt.Println(msg) },
302 Load: loader,
303 }
304 absPath, err := filepath.Abs(filename)
305 if err == nil {
306 mainThread.SetLocal(callerDirKey, filepath.Dir(absPath))
307 _, err = starlark.ExecFile(mainThread, absPath, src, builtins)
308 }
309 return err
310}