1// 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 "io/fs" 20 "os" 21 "os/exec" 22 "path/filepath" 23 "sort" 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// 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 } 141 // 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) 145 return makeStringList(files), nil 146} 147 148// 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 188// 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, 200 fmt.Errorf("cannot run shell, /bin/sh is missing (running on Windows?)") 201 } 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 226// propsetFromEnv constructs a propset from the array of KEY=value strings 227func structFromEnv(env []string) *starlarkstruct.Struct { 228 sd := make(map[string]starlark.Value, len(env)) 229 for _, x := range env { 230 kv := strings.SplitN(x, "=", 2) 231 sd[kv[0]] = starlark.String(kv[1]) 232 } 233 return starlarkstruct.FromStringDict(starlarkstruct.Default, sd) 234} 235 236func log(thread *starlark.Thread, fn *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) { 237 sep := " " 238 if err := starlark.UnpackArgs("print", nil, kwargs, "sep?", &sep); err != nil { 239 return nil, err 240 } 241 for i, v := range args { 242 if i > 0 { 243 fmt.Fprint(os.Stderr, sep) 244 } 245 if s, ok := starlark.AsString(v); ok { 246 fmt.Fprint(os.Stderr, s) 247 } else if b, ok := v.(starlark.Bytes); ok { 248 fmt.Fprint(os.Stderr, string(b)) 249 } else { 250 fmt.Fprintf(os.Stderr, "%s", v) 251 } 252 } 253 254 fmt.Fprintln(os.Stderr) 255 return starlark.None, nil 256} 257 258func setup(env []string) { 259 // Create the symbols that aid makefile conversion. See README.md 260 builtins = starlark.StringDict{ 261 "struct": starlark.NewBuiltin("struct", starlarkstruct.Make), 262 "rblf_cli": structFromEnv(env), 263 "rblf_env": structFromEnv(os.Environ()), 264 // To convert find-copy-subdir and product-copy-files-by pattern 265 "rblf_find_files": starlark.NewBuiltin("rblf_find_files", find), 266 // To convert makefile's $(shell cmd) 267 "rblf_shell": starlark.NewBuiltin("rblf_shell", shell), 268 // Output to stderr 269 "rblf_log": starlark.NewBuiltin("rblf_log", log), 270 // To convert makefile's $(wildcard foo*) 271 "rblf_wildcard": starlark.NewBuiltin("rblf_wildcard", wildcard), 272 } 273 274 // NOTE(asmundak): OS-specific. Behave similar to Linux `system` call, 275 // which always uses /bin/sh to run the command 276 shellPath = "/bin/sh" 277 if _, err := os.Stat(shellPath); err != nil { 278 shellPath = "" 279 } 280} 281 282// Parses, resolves, and executes a Starlark file. 283// filename and src parameters are as for starlark.ExecFile: 284// * filename is the name of the file to execute, 285// and the name that appears in error messages; 286// * src is an optional source of bytes to use instead of filename 287// (it can be a string, or a byte array, or an io.Reader instance) 288// * commandVars is an array of "VAR=value" items. They are accessible from 289// the starlark script as members of the `rblf_cli` propset. 290func Run(filename string, src interface{}, commandVars []string) error { 291 setup(commandVars) 292 293 mainThread := &starlark.Thread{ 294 Name: "main", 295 Print: func(_ *starlark.Thread, msg string) { fmt.Println(msg) }, 296 Load: loader, 297 } 298 absPath, err := filepath.Abs(filename) 299 if err == nil { 300 mainThread.SetLocal(callerDirKey, filepath.Dir(absPath)) 301 _, err = starlark.ExecFile(mainThread, absPath, src, builtins) 302 } 303 return err 304} 305