| Sasha Smundak | 24159db | 2020-10-26 15:43:21 -0700 | [diff] [blame] | 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 |  | 
 | 15 | package rbcrun | 
 | 16 |  | 
 | 17 | import ( | 
 | 18 | 	"fmt" | 
| Sasha Smundak | 6b795dc | 2021-08-18 16:32:19 -0700 | [diff] [blame] | 19 | 	"io/fs" | 
| Sasha Smundak | 24159db | 2020-10-26 15:43:21 -0700 | [diff] [blame] | 20 | 	"os" | 
 | 21 | 	"os/exec" | 
 | 22 | 	"path/filepath" | 
| Cole Faust | c7b8b6e | 2022-04-26 12:03:19 -0700 | [diff] [blame] | 23 | 	"sort" | 
| Sasha Smundak | 24159db | 2020-10-26 15:43:21 -0700 | [diff] [blame] | 24 | 	"strings" | 
 | 25 |  | 
 | 26 | 	"go.starlark.net/starlark" | 
 | 27 | 	"go.starlark.net/starlarkstruct" | 
 | 28 | ) | 
 | 29 |  | 
 | 30 | const callerDirKey = "callerDir" | 
 | 31 |  | 
 | 32 | var LoadPathRoot = "." | 
 | 33 | var shellPath string | 
 | 34 |  | 
 | 35 | type modentry struct { | 
 | 36 | 	globals starlark.StringDict | 
 | 37 | 	err     error | 
 | 38 | } | 
 | 39 |  | 
 | 40 | var moduleCache = make(map[string]*modentry) | 
 | 41 |  | 
 | 42 | var builtins starlark.StringDict | 
 | 43 |  | 
 | 44 | func 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. | 
 | 63 | func 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 Smundak | 24159db | 2020-10-26 15:43:21 -0700 | [diff] [blame] | 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. | 
 | 117 | func 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 Faust | c7b8b6e | 2022-04-26 12:03:19 -0700 | [diff] [blame] | 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) | 
| Sasha Smundak | 24159db | 2020-10-26 15:43:21 -0700 | [diff] [blame] | 145 | 	return makeStringList(files), nil | 
 | 146 | } | 
 | 147 |  | 
| Sasha Smundak | 6b795dc | 2021-08-18 16:32:19 -0700 | [diff] [blame] | 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'. | 
 | 152 | func 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 Smundak | 24159db | 2020-10-26 15:43:21 -0700 | [diff] [blame] | 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. | 
 | 192 | func 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 Smundak | 57bb508 | 2021-04-01 15:51:56 -0700 | [diff] [blame] | 200 | 			fmt.Errorf("cannot run shell, /bin/sh is missing (running on Windows?)") | 
| Sasha Smundak | 24159db | 2020-10-26 15:43:21 -0700 | [diff] [blame] | 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 |  | 
 | 218 | func 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 Smundak | e8652d4 | 2021-09-24 08:25:17 -0700 | [diff] [blame] | 226 | func 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 Faust | a874f88 | 2023-05-05 11:46:51 -0700 | [diff] [blame] | 248 | func setup() { | 
| Sasha Smundak | 24159db | 2020-10-26 15:43:21 -0700 | [diff] [blame] | 249 | 	// Create the symbols that aid makefile conversion. See README.md | 
 | 250 | 	builtins = starlark.StringDict{ | 
 | 251 | 		"struct":   starlark.NewBuiltin("struct", starlarkstruct.Make), | 
| Sasha Smundak | 6b795dc | 2021-08-18 16:32:19 -0700 | [diff] [blame] | 252 | 		// To convert find-copy-subdir and product-copy-files-by pattern | 
 | 253 | 		"rblf_find_files": starlark.NewBuiltin("rblf_find_files", find), | 
| Sasha Smundak | 24159db | 2020-10-26 15:43:21 -0700 | [diff] [blame] | 254 | 		// To convert makefile's $(shell cmd) | 
 | 255 | 		"rblf_shell": starlark.NewBuiltin("rblf_shell", shell), | 
| Sasha Smundak | e8652d4 | 2021-09-24 08:25:17 -0700 | [diff] [blame] | 256 | 		// Output to stderr | 
 | 257 | 		"rblf_log": starlark.NewBuiltin("rblf_log", log), | 
| Sasha Smundak | 24159db | 2020-10-26 15:43:21 -0700 | [diff] [blame] | 258 | 		// To convert makefile's $(wildcard foo*) | 
 | 259 | 		"rblf_wildcard": starlark.NewBuiltin("rblf_wildcard", wildcard), | 
 | 260 | 	} | 
 | 261 |  | 
| Sasha Smundak | 57bb508 | 2021-04-01 15:51:56 -0700 | [diff] [blame] | 262 | 	// 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 Smundak | 24159db | 2020-10-26 15:43:21 -0700 | [diff] [blame] | 268 | } | 
 | 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 Faust | a874f88 | 2023-05-05 11:46:51 -0700 | [diff] [blame] | 276 | func Run(filename string, src interface{}) error { | 
 | 277 | 	setup() | 
| Sasha Smundak | 24159db | 2020-10-26 15:43:21 -0700 | [diff] [blame] | 278 | 	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 | } |