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 main
16
17import (
18	"flag"
19	"fmt"
20	"os"
21	"rbcrun"
22	"regexp"
23	"strings"
24
25	"go.starlark.net/starlark"
26)
27
28var (
29	allowExternalEntrypoint = flag.Bool("allow_external_entrypoint", false, "allow the entrypoint starlark file to be outside of the source tree")
30	modeFlag  = flag.String("mode", "", "the general behavior of rbcrun. Can be \"rbc\" or \"make\". Required.")
31	rootdir  = flag.String("d", ".", "the value of // for load paths")
32	perfFile = flag.String("perf", "", "save performance data")
33	identifierRe = regexp.MustCompile("[a-zA-Z_][a-zA-Z0-9_]*")
34)
35
36func getEntrypointStarlarkFile() string {
37	filename := ""
38
39	for _, arg := range flag.Args() {
40		if filename == "" {
41			filename = arg
42		} else {
43			quit("only one file can be executed\n")
44		}
45	}
46	if filename == "" {
47		flag.Usage()
48		os.Exit(1)
49	}
50	return filename
51}
52
53func getMode() rbcrun.ExecutionMode {
54	switch *modeFlag {
55	case "rbc":
56		return rbcrun.ExecutionModeRbc
57	case "make":
58		return rbcrun.ExecutionModeScl
59	case "":
60		quit("-mode flag is required.")
61	default:
62		quit("Unknown -mode value %q, expected 1 of \"rbc\", \"make\"", *modeFlag)
63	}
64	return rbcrun.ExecutionModeScl
65}
66
67var makeStringReplacer = strings.NewReplacer("#", "\\#", "$", "$$")
68
69func cleanStringForMake(s string) (string, error) {
70	if strings.ContainsAny(s, "\\\n") {
71		// \\ in make is literally \\, not a single \, so we can't allow them.
72		// \<newline> in make will produce a space, not a newline.
73		return "", fmt.Errorf("starlark strings exported to make cannot contain backslashes or newlines")
74	}
75	return makeStringReplacer.Replace(s), nil
76}
77
78func getValueInMakeFormat(value starlark.Value, allowLists bool) (string, error) {
79	switch v := value.(type) {
80	case starlark.String:
81		if cleanedValue, err := cleanStringForMake(v.GoString()); err == nil {
82			return cleanedValue, nil
83		} else {
84			return "", err
85		}
86	case starlark.Int:
87		return v.String(), nil
88	case *starlark.List:
89		if !allowLists {
90			return "", fmt.Errorf("nested lists are not allowed to be exported from starlark to make, flatten the list in starlark first")
91		}
92		result := ""
93		for i := 0; i < v.Len(); i++ {
94			value, err := getValueInMakeFormat(v.Index(i), false)
95			if err != nil {
96				return "", err
97			}
98			if i > 0 {
99				result += " "
100			}
101			result += value
102		}
103		return result, nil
104	default:
105		return "", fmt.Errorf("only starlark strings, ints, and lists of strings/ints can be exported to make. Please convert all other types in starlark first. Found type: %s", value.Type())
106	}
107}
108
109func printVarsInMakeFormat(globals starlark.StringDict) error {
110	// We could just directly export top level variables by name instead of going through
111	// a variables_to_export_to_make dictionary, but that wouldn't allow for exporting a
112	// runtime-defined number of variables to make. This can be important because dictionaries
113	// in make are often represented by a unique variable for every key in the dictionary.
114	variablesValue, ok := globals["variables_to_export_to_make"]
115	if !ok {
116		return fmt.Errorf("expected top-level starlark file to have a \"variables_to_export_to_make\" variable")
117	}
118	variables, ok := variablesValue.(*starlark.Dict)
119	if !ok {
120		return fmt.Errorf("expected variables_to_export_to_make to be a dict, got %s", variablesValue.Type())
121	}
122
123	for _, varTuple := range variables.Items() {
124		varNameStarlark, ok := varTuple.Index(0).(starlark.String)
125		if !ok {
126			return fmt.Errorf("all keys in variables_to_export_to_make must be strings, but got %q", varTuple.Index(0).Type())
127		}
128		varName := varNameStarlark.GoString()
129		if !identifierRe.MatchString(varName) {
130			return fmt.Errorf("all variables at the top level starlark file must be valid c identifiers, but got %q", varName)
131		}
132		if varName == "LOADED_STARLARK_FILES" {
133			return fmt.Errorf("the name LOADED_STARLARK_FILES is reserved for use by the starlark interpreter")
134		}
135		valueMake, err := getValueInMakeFormat(varTuple.Index(1), true)
136		if err != nil {
137			return err
138		}
139		// The :=$= is special Kati syntax that means "set and make readonly"
140		fmt.Printf("%s :=$= %s\n", varName, valueMake)
141	}
142	return nil
143}
144
145func main() {
146	flag.Parse()
147	filename := getEntrypointStarlarkFile()
148	mode := getMode()
149
150	if os.Chdir(*rootdir) != nil {
151		quit("could not chdir to %s\n", *rootdir)
152	}
153	if *perfFile != "" {
154		pprof, err := os.Create(*perfFile)
155		if err != nil {
156			quit("%s: err", *perfFile)
157		}
158		defer pprof.Close()
159		if err := starlark.StartProfile(pprof); err != nil {
160			quit("%s\n", err)
161		}
162	}
163	variables, loadedStarlarkFiles, err := rbcrun.Run(filename, nil, mode, *allowExternalEntrypoint)
164	rc := 0
165	if *perfFile != "" {
166		if err2 := starlark.StopProfile(); err2 != nil {
167			fmt.Fprintln(os.Stderr, err2)
168			rc = 1
169		}
170	}
171	if err != nil {
172		if evalErr, ok := err.(*starlark.EvalError); ok {
173			quit("%s\n", evalErr.Backtrace())
174		} else {
175			quit("%s\n", err)
176		}
177	}
178	if mode == rbcrun.ExecutionModeScl {
179		if err := printVarsInMakeFormat(variables); err != nil {
180			quit("%s\n", err)
181		}
182		fmt.Printf("LOADED_STARLARK_FILES := %s\n", strings.Join(loadedStarlarkFiles, " "))
183	}
184	os.Exit(rc)
185}
186
187func quit(format string, s ...interface{}) {
188	fmt.Fprintf(os.Stderr, format, s...)
189	os.Exit(2)
190}
191