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