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 "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 } 121 if _, err := os.Stat(path); err != nil { 122 return starlark.False, nil 123 } 124 return starlark.True, nil 125} 126 127// 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 156// 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 196// 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, 208 fmt.Errorf("cannot run shell, /bin/sh is missing (running on Windows?)") 209 } 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 244func 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 266func 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), 274 // To convert find-copy-subdir and product-copy-files-by pattern 275 "rblf_find_files": starlark.NewBuiltin("rblf_find_files", find), 276 // To convert makefile's $(shell cmd) 277 "rblf_shell": starlark.NewBuiltin("rblf_shell", shell), 278 // Output to stderr 279 "rblf_log": starlark.NewBuiltin("rblf_log", log), 280 // To convert makefile's $(wildcard foo*) 281 "rblf_wildcard": starlark.NewBuiltin("rblf_wildcard", wildcard), 282 } 283 284 // 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 } 290} 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} 315