blob: f2fda4e1455fef9170fe40e6225b7e9963421db9 [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"
Cole Faustc7b8b6e2022-04-26 12:03:19 -070023 "sort"
Sasha Smundak24159db2020-10-26 15:43:21 -070024 "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
Sasha Smundak24159db2020-10-26 15:43:21 -0700115// wildcard(pattern, top=None) expands shell's glob pattern. If 'top' is present,
116// the 'top/pattern' is globbed and then 'top/' prefix is removed.
117func wildcard(_ *starlark.Thread, b *starlark.Builtin, args starlark.Tuple,
118 kwargs []starlark.Tuple) (starlark.Value, error) {
119 var pattern string
120 var top string
121
122 if err := starlark.UnpackPositionalArgs(b.Name(), args, kwargs, 1, &pattern, &top); err != nil {
123 return starlark.None, err
124 }
125
126 var files []string
127 var err error
128 if top == "" {
129 if files, err = filepath.Glob(pattern); err != nil {
130 return starlark.None, err
131 }
132 } else {
133 prefix := top + string(filepath.Separator)
134 if files, err = filepath.Glob(prefix + pattern); err != nil {
135 return starlark.None, err
136 }
137 for i := range files {
138 files[i] = strings.TrimPrefix(files[i], prefix)
139 }
140 }
Cole Faustc7b8b6e2022-04-26 12:03:19 -0700141 // Kati uses glob(3) with no flags, which means it's sorted
142 // because GLOB_NOSORT is not passed. Go's glob is not
143 // guaranteed to sort the results.
144 sort.Strings(files)
Sasha Smundak24159db2020-10-26 15:43:21 -0700145 return makeStringList(files), nil
146}
147
Sasha Smundak6b795dc2021-08-18 16:32:19 -0700148// find(top, pattern, only_files = 0) returns all the paths under 'top'
149// whose basename matches 'pattern' (which is a shell's glob pattern).
150// If 'only_files' is non-zero, only the paths to the regular files are
151// returned. The returned paths are relative to 'top'.
152func find(_ *starlark.Thread, b *starlark.Builtin, args starlark.Tuple,
153 kwargs []starlark.Tuple) (starlark.Value, error) {
154 var top, pattern string
155 var onlyFiles int
156 if err := starlark.UnpackArgs(b.Name(), args, kwargs,
157 "top", &top, "pattern", &pattern, "only_files?", &onlyFiles); err != nil {
158 return starlark.None, err
159 }
160 top = filepath.Clean(top)
161 pattern = filepath.Clean(pattern)
162 // Go's filepath.Walk is slow, consider using OS's find
163 var res []string
164 err := filepath.WalkDir(top, func(path string, d fs.DirEntry, err error) error {
165 if err != nil {
166 if d != nil && d.IsDir() {
167 return fs.SkipDir
168 } else {
169 return nil
170 }
171 }
172 relPath := strings.TrimPrefix(path, top)
173 if len(relPath) > 0 && relPath[0] == os.PathSeparator {
174 relPath = relPath[1:]
175 }
176 // Do not return top-level dir
177 if len(relPath) == 0 {
178 return nil
179 }
180 if matched, err := filepath.Match(pattern, d.Name()); err == nil && matched && (onlyFiles == 0 || d.Type().IsRegular()) {
181 res = append(res, relPath)
182 }
183 return nil
184 })
185 return makeStringList(res), err
186}
187
Sasha Smundak24159db2020-10-26 15:43:21 -0700188// shell(command) runs OS shell with given command and returns back
189// its output the same way as Make's $(shell ) function. The end-of-lines
190// ("\n" or "\r\n") are replaced with " " in the result, and the trailing
191// end-of-line is removed.
192func shell(_ *starlark.Thread, b *starlark.Builtin, args starlark.Tuple,
193 kwargs []starlark.Tuple) (starlark.Value, error) {
194 var command string
195 if err := starlark.UnpackPositionalArgs(b.Name(), args, kwargs, 1, &command); err != nil {
196 return starlark.None, err
197 }
198 if shellPath == "" {
199 return starlark.None,
Sasha Smundak57bb5082021-04-01 15:51:56 -0700200 fmt.Errorf("cannot run shell, /bin/sh is missing (running on Windows?)")
Sasha Smundak24159db2020-10-26 15:43:21 -0700201 }
202 cmd := exec.Command(shellPath, "-c", command)
203 // We ignore command's status
204 bytes, _ := cmd.Output()
205 output := string(bytes)
206 if strings.HasSuffix(output, "\n") {
207 output = strings.TrimSuffix(output, "\n")
208 } else {
209 output = strings.TrimSuffix(output, "\r\n")
210 }
211
212 return starlark.String(
213 strings.ReplaceAll(
214 strings.ReplaceAll(output, "\r\n", " "),
215 "\n", " ")), nil
216}
217
218func makeStringList(items []string) *starlark.List {
219 elems := make([]starlark.Value, len(items))
220 for i, item := range items {
221 elems[i] = starlark.String(item)
222 }
223 return starlark.NewList(elems)
224}
225
Sasha Smundake8652d42021-09-24 08:25:17 -0700226func log(thread *starlark.Thread, fn *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) {
227 sep := " "
228 if err := starlark.UnpackArgs("print", nil, kwargs, "sep?", &sep); err != nil {
229 return nil, err
230 }
231 for i, v := range args {
232 if i > 0 {
233 fmt.Fprint(os.Stderr, sep)
234 }
235 if s, ok := starlark.AsString(v); ok {
236 fmt.Fprint(os.Stderr, s)
237 } else if b, ok := v.(starlark.Bytes); ok {
238 fmt.Fprint(os.Stderr, string(b))
239 } else {
240 fmt.Fprintf(os.Stderr, "%s", v)
241 }
242 }
243
244 fmt.Fprintln(os.Stderr)
245 return starlark.None, nil
246}
247
Cole Fausta874f882023-05-05 11:46:51 -0700248func setup() {
Sasha Smundak24159db2020-10-26 15:43:21 -0700249 // Create the symbols that aid makefile conversion. See README.md
250 builtins = starlark.StringDict{
251 "struct": starlark.NewBuiltin("struct", starlarkstruct.Make),
Sasha Smundak6b795dc2021-08-18 16:32:19 -0700252 // To convert find-copy-subdir and product-copy-files-by pattern
253 "rblf_find_files": starlark.NewBuiltin("rblf_find_files", find),
Sasha Smundak24159db2020-10-26 15:43:21 -0700254 // To convert makefile's $(shell cmd)
255 "rblf_shell": starlark.NewBuiltin("rblf_shell", shell),
Sasha Smundake8652d42021-09-24 08:25:17 -0700256 // Output to stderr
257 "rblf_log": starlark.NewBuiltin("rblf_log", log),
Sasha Smundak24159db2020-10-26 15:43:21 -0700258 // To convert makefile's $(wildcard foo*)
259 "rblf_wildcard": starlark.NewBuiltin("rblf_wildcard", wildcard),
260 }
261
Sasha Smundak57bb5082021-04-01 15:51:56 -0700262 // NOTE(asmundak): OS-specific. Behave similar to Linux `system` call,
263 // which always uses /bin/sh to run the command
264 shellPath = "/bin/sh"
265 if _, err := os.Stat(shellPath); err != nil {
266 shellPath = ""
267 }
Sasha Smundak24159db2020-10-26 15:43:21 -0700268}
269
270// Parses, resolves, and executes a Starlark file.
271// filename and src parameters are as for starlark.ExecFile:
272// * filename is the name of the file to execute,
273// and the name that appears in error messages;
274// * src is an optional source of bytes to use instead of filename
275// (it can be a string, or a byte array, or an io.Reader instance)
Cole Fausta874f882023-05-05 11:46:51 -0700276func Run(filename string, src interface{}) error {
277 setup()
Sasha Smundak24159db2020-10-26 15:43:21 -0700278 mainThread := &starlark.Thread{
279 Name: "main",
280 Print: func(_ *starlark.Thread, msg string) { fmt.Println(msg) },
281 Load: loader,
282 }
283 absPath, err := filepath.Abs(filename)
284 if err == nil {
285 mainThread.SetLocal(callerDirKey, filepath.Dir(absPath))
286 _, err = starlark.ExecFile(mainThread, absPath, src, builtins)
287 }
288 return err
289}