• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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