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 "os" 20 "os/exec" 21 "path/filepath" 22 "regexp" 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 stat, err := os.Stat(path); err != nil || stat.IsDir() { 122 return starlark.False, nil 123 } 124 return starlark.True, nil 125} 126 127// regexMatch(pattern, s) returns True if s matches pattern (a regex) 128func regexMatch(_ *starlark.Thread, b *starlark.Builtin, args starlark.Tuple, 129 kwargs []starlark.Tuple) (starlark.Value, error) { 130 var pattern, s string 131 if err := starlark.UnpackPositionalArgs(b.Name(), args, kwargs, 2, &pattern, &s); err != nil { 132 return starlark.None, err 133 } 134 match, err := regexp.MatchString(pattern, s) 135 if err != nil { 136 return starlark.None, err 137 } 138 if match { 139 return starlark.True, nil 140 } 141 return starlark.False, nil 142} 143 144// wildcard(pattern, top=None) expands shell's glob pattern. If 'top' is present, 145// the 'top/pattern' is globbed and then 'top/' prefix is removed. 146func wildcard(_ *starlark.Thread, b *starlark.Builtin, args starlark.Tuple, 147 kwargs []starlark.Tuple) (starlark.Value, error) { 148 var pattern string 149 var top string 150 151 if err := starlark.UnpackPositionalArgs(b.Name(), args, kwargs, 1, &pattern, &top); err != nil { 152 return starlark.None, err 153 } 154 155 var files []string 156 var err error 157 if top == "" { 158 if files, err = filepath.Glob(pattern); err != nil { 159 return starlark.None, err 160 } 161 } else { 162 prefix := top + string(filepath.Separator) 163 if files, err = filepath.Glob(prefix + pattern); err != nil { 164 return starlark.None, err 165 } 166 for i := range files { 167 files[i] = strings.TrimPrefix(files[i], prefix) 168 } 169 } 170 return makeStringList(files), nil 171} 172 173// shell(command) runs OS shell with given command and returns back 174// its output the same way as Make's $(shell ) function. The end-of-lines 175// ("\n" or "\r\n") are replaced with " " in the result, and the trailing 176// end-of-line is removed. 177func shell(_ *starlark.Thread, b *starlark.Builtin, args starlark.Tuple, 178 kwargs []starlark.Tuple) (starlark.Value, error) { 179 var command string 180 if err := starlark.UnpackPositionalArgs(b.Name(), args, kwargs, 1, &command); err != nil { 181 return starlark.None, err 182 } 183 if shellPath == "" { 184 return starlark.None, 185 fmt.Errorf("cannot run shell, /bin/sh is missing (running on Windows?)") 186 } 187 cmd := exec.Command(shellPath, "-c", command) 188 // We ignore command's status 189 bytes, _ := cmd.Output() 190 output := string(bytes) 191 if strings.HasSuffix(output, "\n") { 192 output = strings.TrimSuffix(output, "\n") 193 } else { 194 output = strings.TrimSuffix(output, "\r\n") 195 } 196 197 return starlark.String( 198 strings.ReplaceAll( 199 strings.ReplaceAll(output, "\r\n", " "), 200 "\n", " ")), nil 201} 202 203func makeStringList(items []string) *starlark.List { 204 elems := make([]starlark.Value, len(items)) 205 for i, item := range items { 206 elems[i] = starlark.String(item) 207 } 208 return starlark.NewList(elems) 209} 210 211// propsetFromEnv constructs a propset from the array of KEY=value strings 212func structFromEnv(env []string) *starlarkstruct.Struct { 213 sd := make(map[string]starlark.Value, len(env)) 214 for _, x := range env { 215 kv := strings.SplitN(x, "=", 2) 216 sd[kv[0]] = starlark.String(kv[1]) 217 } 218 return starlarkstruct.FromStringDict(starlarkstruct.Default, sd) 219} 220 221func setup(env []string) { 222 // Create the symbols that aid makefile conversion. See README.md 223 builtins = starlark.StringDict{ 224 "struct": starlark.NewBuiltin("struct", starlarkstruct.Make), 225 "rblf_cli": structFromEnv(env), 226 "rblf_env": structFromEnv(os.Environ()), 227 // To convert makefile's $(wildcard foo) 228 "rblf_file_exists": starlark.NewBuiltin("rblf_file_exists", fileExists), 229 // To convert makefile's $(filter ...)/$(filter-out) 230 "rblf_regex": starlark.NewBuiltin("rblf_regex", regexMatch), 231 // To convert makefile's $(shell cmd) 232 "rblf_shell": starlark.NewBuiltin("rblf_shell", shell), 233 // To convert makefile's $(wildcard foo*) 234 "rblf_wildcard": starlark.NewBuiltin("rblf_wildcard", wildcard), 235 } 236 237 // NOTE(asmundak): OS-specific. Behave similar to Linux `system` call, 238 // which always uses /bin/sh to run the command 239 shellPath = "/bin/sh" 240 if _, err := os.Stat(shellPath); err != nil { 241 shellPath = "" 242 } 243} 244 245// Parses, resolves, and executes a Starlark file. 246// filename and src parameters are as for starlark.ExecFile: 247// * filename is the name of the file to execute, 248// and the name that appears in error messages; 249// * src is an optional source of bytes to use instead of filename 250// (it can be a string, or a byte array, or an io.Reader instance) 251// * commandVars is an array of "VAR=value" items. They are accessible from 252// the starlark script as members of the `rblf_cli` propset. 253func Run(filename string, src interface{}, commandVars []string) error { 254 setup(commandVars) 255 256 mainThread := &starlark.Thread{ 257 Name: "main", 258 Print: func(_ *starlark.Thread, msg string) { fmt.Println(msg) }, 259 Load: loader, 260 } 261 absPath, err := filepath.Abs(filename) 262 if err == nil { 263 mainThread.SetLocal(callerDirKey, filepath.Dir(absPath)) 264 _, err = starlark.ExecFile(mainThread, absPath, src, builtins) 265 } 266 return err 267} 268