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