| Cole Faust | c9508aa | 2023-02-07 11:38:27 -0800 | [diff] [blame] | 1 | // Copyright 2023 Google Inc. All rights reserved. | 
 | 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 starlark_import | 
 | 16 |  | 
 | 17 | import ( | 
 | 18 | 	"fmt" | 
 | 19 | 	"os" | 
 | 20 | 	"path/filepath" | 
 | 21 | 	"sort" | 
 | 22 | 	"strings" | 
 | 23 | 	"sync" | 
 | 24 | 	"time" | 
 | 25 |  | 
 | 26 | 	"go.starlark.net/starlark" | 
 | 27 | 	"go.starlark.net/starlarkjson" | 
 | 28 | 	"go.starlark.net/starlarkstruct" | 
 | 29 | ) | 
 | 30 |  | 
 | 31 | func init() { | 
 | 32 | 	go func() { | 
 | 33 | 		startTime := time.Now() | 
 | 34 | 		v, d, err := runStarlarkFile("//build/bazel/constants_exported_to_soong.bzl") | 
 | 35 | 		endTime := time.Now() | 
 | 36 | 		//fmt.Fprintf(os.Stderr, "starlark run time: %s\n", endTime.Sub(startTime).String()) | 
 | 37 | 		globalResult.Set(starlarkResult{ | 
 | 38 | 			values:    v, | 
 | 39 | 			ninjaDeps: d, | 
 | 40 | 			err:       err, | 
 | 41 | 			startTime: startTime, | 
 | 42 | 			endTime:   endTime, | 
 | 43 | 		}) | 
 | 44 | 	}() | 
 | 45 | } | 
 | 46 |  | 
 | 47 | type starlarkResult struct { | 
 | 48 | 	values    starlark.StringDict | 
 | 49 | 	ninjaDeps []string | 
 | 50 | 	err       error | 
 | 51 | 	startTime time.Time | 
 | 52 | 	endTime   time.Time | 
 | 53 | } | 
 | 54 |  | 
 | 55 | // setOnce wraps a value and exposes Set() and Get() accessors for it. | 
 | 56 | // The Get() calls will block until a Set() has been called. | 
 | 57 | // A second call to Set() will panic. | 
 | 58 | // setOnce must be created using newSetOnce() | 
 | 59 | type setOnce[T any] struct { | 
 | 60 | 	value T | 
 | 61 | 	lock  sync.Mutex | 
 | 62 | 	wg    sync.WaitGroup | 
 | 63 | 	isSet bool | 
 | 64 | } | 
 | 65 |  | 
 | 66 | func (o *setOnce[T]) Set(value T) { | 
 | 67 | 	o.lock.Lock() | 
 | 68 | 	defer o.lock.Unlock() | 
 | 69 | 	if o.isSet { | 
 | 70 | 		panic("Value already set") | 
 | 71 | 	} | 
 | 72 |  | 
 | 73 | 	o.value = value | 
 | 74 | 	o.isSet = true | 
 | 75 | 	o.wg.Done() | 
 | 76 | } | 
 | 77 |  | 
 | 78 | func (o *setOnce[T]) Get() T { | 
 | 79 | 	if !o.isSet { | 
 | 80 | 		o.wg.Wait() | 
 | 81 | 	} | 
 | 82 | 	return o.value | 
 | 83 | } | 
 | 84 |  | 
 | 85 | func newSetOnce[T any]() *setOnce[T] { | 
 | 86 | 	result := &setOnce[T]{} | 
 | 87 | 	result.wg.Add(1) | 
 | 88 | 	return result | 
 | 89 | } | 
 | 90 |  | 
 | 91 | var globalResult = newSetOnce[starlarkResult]() | 
 | 92 |  | 
 | 93 | func GetStarlarkValue[T any](key string) (T, error) { | 
 | 94 | 	result := globalResult.Get() | 
 | 95 | 	if result.err != nil { | 
 | 96 | 		var zero T | 
 | 97 | 		return zero, result.err | 
 | 98 | 	} | 
 | 99 | 	if !result.values.Has(key) { | 
 | 100 | 		var zero T | 
 | 101 | 		return zero, fmt.Errorf("a starlark variable by that name wasn't found, did you update //build/bazel/constants_exported_to_soong.bzl?") | 
 | 102 | 	} | 
 | 103 | 	return Unmarshal[T](result.values[key]) | 
 | 104 | } | 
 | 105 |  | 
 | 106 | func GetNinjaDeps() ([]string, error) { | 
 | 107 | 	result := globalResult.Get() | 
 | 108 | 	if result.err != nil { | 
 | 109 | 		return nil, result.err | 
 | 110 | 	} | 
 | 111 | 	return result.ninjaDeps, nil | 
 | 112 | } | 
 | 113 |  | 
 | 114 | func getTopDir() (string, error) { | 
 | 115 | 	// It's hard to communicate the top dir to this package in any other way than reading the | 
 | 116 | 	// arguments directly, because we need to know this at package initialization time. Many | 
 | 117 | 	// soong constants that we'd like to read from starlark are initialized during package | 
 | 118 | 	// initialization. | 
 | 119 | 	for i, arg := range os.Args { | 
 | 120 | 		if arg == "--top" { | 
 | 121 | 			if i < len(os.Args)-1 && os.Args[i+1] != "" { | 
 | 122 | 				return os.Args[i+1], nil | 
 | 123 | 			} | 
 | 124 | 		} | 
 | 125 | 	} | 
 | 126 |  | 
 | 127 | 	// When running tests, --top is not passed. Instead, search for the top dir manually | 
 | 128 | 	cwd, err := os.Getwd() | 
 | 129 | 	if err != nil { | 
 | 130 | 		return "", err | 
 | 131 | 	} | 
 | 132 | 	for cwd != "/" { | 
 | 133 | 		if _, err := os.Stat(filepath.Join(cwd, "build/soong/soong_ui.bash")); err == nil { | 
 | 134 | 			return cwd, nil | 
 | 135 | 		} | 
 | 136 | 		cwd = filepath.Dir(cwd) | 
 | 137 | 	} | 
 | 138 | 	return "", fmt.Errorf("could not find top dir") | 
 | 139 | } | 
 | 140 |  | 
 | 141 | const callerDirKey = "callerDir" | 
 | 142 |  | 
 | 143 | type modentry struct { | 
 | 144 | 	globals starlark.StringDict | 
 | 145 | 	err     error | 
 | 146 | } | 
 | 147 |  | 
 | 148 | func unsupportedMethod(t *starlark.Thread, fn *starlark.Builtin, _ starlark.Tuple, _ []starlark.Tuple) (starlark.Value, error) { | 
 | 149 | 	return nil, fmt.Errorf("%sthis file is read by soong, and must therefore be pure starlark and include only constant information. %q is not allowed", t.CallStack().String(), fn.Name()) | 
 | 150 | } | 
 | 151 |  | 
 | 152 | var builtins = starlark.StringDict{ | 
 | 153 | 	"aspect":     starlark.NewBuiltin("aspect", unsupportedMethod), | 
 | 154 | 	"glob":       starlark.NewBuiltin("glob", unsupportedMethod), | 
 | 155 | 	"json":       starlarkjson.Module, | 
 | 156 | 	"provider":   starlark.NewBuiltin("provider", unsupportedMethod), | 
 | 157 | 	"rule":       starlark.NewBuiltin("rule", unsupportedMethod), | 
 | 158 | 	"struct":     starlark.NewBuiltin("struct", starlarkstruct.Make), | 
 | 159 | 	"select":     starlark.NewBuiltin("select", unsupportedMethod), | 
 | 160 | 	"transition": starlark.NewBuiltin("transition", unsupportedMethod), | 
 | 161 | } | 
 | 162 |  | 
 | 163 | // Takes a module name (the first argument to the load() function) and returns the path | 
 | 164 | // it's trying to load, stripping out leading //, and handling leading :s. | 
 | 165 | func cleanModuleName(moduleName string, callerDir string) (string, error) { | 
 | 166 | 	if strings.Count(moduleName, ":") > 1 { | 
 | 167 | 		return "", fmt.Errorf("at most 1 colon must be present in starlark path: %s", moduleName) | 
 | 168 | 	} | 
 | 169 |  | 
 | 170 | 	// We don't have full support for external repositories, but at least support skylib's dicts. | 
 | 171 | 	if moduleName == "@bazel_skylib//lib:dicts.bzl" { | 
 | 172 | 		return "external/bazel-skylib/lib/dicts.bzl", nil | 
 | 173 | 	} | 
 | 174 |  | 
 | 175 | 	localLoad := false | 
 | 176 | 	if strings.HasPrefix(moduleName, "@//") { | 
 | 177 | 		moduleName = moduleName[3:] | 
 | 178 | 	} else if strings.HasPrefix(moduleName, "//") { | 
 | 179 | 		moduleName = moduleName[2:] | 
 | 180 | 	} else if strings.HasPrefix(moduleName, ":") { | 
 | 181 | 		moduleName = moduleName[1:] | 
 | 182 | 		localLoad = true | 
 | 183 | 	} else { | 
 | 184 | 		return "", fmt.Errorf("load path must start with // or :") | 
 | 185 | 	} | 
 | 186 |  | 
 | 187 | 	if ix := strings.LastIndex(moduleName, ":"); ix >= 0 { | 
 | 188 | 		moduleName = moduleName[:ix] + string(os.PathSeparator) + moduleName[ix+1:] | 
 | 189 | 	} | 
 | 190 |  | 
 | 191 | 	if filepath.Clean(moduleName) != moduleName { | 
 | 192 | 		return "", fmt.Errorf("load path must be clean, found: %s, expected: %s", moduleName, filepath.Clean(moduleName)) | 
 | 193 | 	} | 
 | 194 | 	if strings.HasPrefix(moduleName, "../") { | 
 | 195 | 		return "", fmt.Errorf("load path must not start with ../: %s", moduleName) | 
 | 196 | 	} | 
 | 197 | 	if strings.HasPrefix(moduleName, "/") { | 
 | 198 | 		return "", fmt.Errorf("load path starts with /, use // for a absolute path: %s", moduleName) | 
 | 199 | 	} | 
 | 200 |  | 
 | 201 | 	if localLoad { | 
 | 202 | 		return filepath.Join(callerDir, moduleName), nil | 
 | 203 | 	} | 
 | 204 |  | 
 | 205 | 	return moduleName, nil | 
 | 206 | } | 
 | 207 |  | 
 | 208 | // loader implements load statement. The format of the loaded module URI is | 
 | 209 | // | 
 | 210 | //	[//path]:base | 
 | 211 | // | 
 | 212 | // The file path is $ROOT/path/base if path is present, <caller_dir>/base otherwise. | 
 | 213 | func loader(thread *starlark.Thread, module string, topDir string, moduleCache map[string]*modentry, moduleCacheLock *sync.Mutex, filesystem map[string]string) (starlark.StringDict, error) { | 
 | 214 | 	modulePath, err := cleanModuleName(module, thread.Local(callerDirKey).(string)) | 
 | 215 | 	if err != nil { | 
 | 216 | 		return nil, err | 
 | 217 | 	} | 
 | 218 | 	moduleCacheLock.Lock() | 
 | 219 | 	e, ok := moduleCache[modulePath] | 
 | 220 | 	if e == nil { | 
 | 221 | 		if ok { | 
 | 222 | 			moduleCacheLock.Unlock() | 
 | 223 | 			return nil, fmt.Errorf("cycle in load graph") | 
 | 224 | 		} | 
 | 225 |  | 
 | 226 | 		// Add a placeholder to indicate "load in progress". | 
 | 227 | 		moduleCache[modulePath] = nil | 
 | 228 | 		moduleCacheLock.Unlock() | 
 | 229 |  | 
 | 230 | 		childThread := &starlark.Thread{Name: "exec " + module, Load: thread.Load} | 
 | 231 |  | 
 | 232 | 		// Cheating for the sake of testing: | 
 | 233 | 		// propagate starlarktest's Reporter key, otherwise testing | 
 | 234 | 		// the load function may cause panic in starlarktest code. | 
 | 235 | 		const testReporterKey = "Reporter" | 
 | 236 | 		if v := thread.Local(testReporterKey); v != nil { | 
 | 237 | 			childThread.SetLocal(testReporterKey, v) | 
 | 238 | 		} | 
 | 239 |  | 
 | 240 | 		childThread.SetLocal(callerDirKey, filepath.Dir(modulePath)) | 
 | 241 |  | 
 | 242 | 		if filesystem != nil { | 
 | 243 | 			globals, err := starlark.ExecFile(childThread, filepath.Join(topDir, modulePath), filesystem[modulePath], builtins) | 
 | 244 | 			e = &modentry{globals, err} | 
 | 245 | 		} else { | 
 | 246 | 			globals, err := starlark.ExecFile(childThread, filepath.Join(topDir, modulePath), nil, builtins) | 
 | 247 | 			e = &modentry{globals, err} | 
 | 248 | 		} | 
 | 249 |  | 
 | 250 | 		// Update the cache. | 
 | 251 | 		moduleCacheLock.Lock() | 
 | 252 | 		moduleCache[modulePath] = e | 
 | 253 | 	} | 
 | 254 | 	moduleCacheLock.Unlock() | 
 | 255 | 	return e.globals, e.err | 
 | 256 | } | 
 | 257 |  | 
 | 258 | // Run runs the given starlark file and returns its global variables and a list of all starlark | 
 | 259 | // files that were loaded. The top dir for starlark's // is found via getTopDir(). | 
 | 260 | func runStarlarkFile(filename string) (starlark.StringDict, []string, error) { | 
 | 261 | 	topDir, err := getTopDir() | 
 | 262 | 	if err != nil { | 
 | 263 | 		return nil, nil, err | 
 | 264 | 	} | 
 | 265 | 	return runStarlarkFileWithFilesystem(filename, topDir, nil) | 
 | 266 | } | 
 | 267 |  | 
 | 268 | func runStarlarkFileWithFilesystem(filename string, topDir string, filesystem map[string]string) (starlark.StringDict, []string, error) { | 
 | 269 | 	if !strings.HasPrefix(filename, "//") && !strings.HasPrefix(filename, ":") { | 
 | 270 | 		filename = "//" + filename | 
 | 271 | 	} | 
 | 272 | 	filename, err := cleanModuleName(filename, "") | 
 | 273 | 	if err != nil { | 
 | 274 | 		return nil, nil, err | 
 | 275 | 	} | 
 | 276 | 	moduleCache := make(map[string]*modentry) | 
 | 277 | 	moduleCache[filename] = nil | 
 | 278 | 	moduleCacheLock := &sync.Mutex{} | 
 | 279 | 	mainThread := &starlark.Thread{ | 
 | 280 | 		Name: "main", | 
 | 281 | 		Print: func(_ *starlark.Thread, msg string) { | 
 | 282 | 			// Ignore prints | 
 | 283 | 		}, | 
 | 284 | 		Load: func(thread *starlark.Thread, module string) (starlark.StringDict, error) { | 
 | 285 | 			return loader(thread, module, topDir, moduleCache, moduleCacheLock, filesystem) | 
 | 286 | 		}, | 
 | 287 | 	} | 
 | 288 | 	mainThread.SetLocal(callerDirKey, filepath.Dir(filename)) | 
 | 289 |  | 
 | 290 | 	var result starlark.StringDict | 
 | 291 | 	if filesystem != nil { | 
 | 292 | 		result, err = starlark.ExecFile(mainThread, filepath.Join(topDir, filename), filesystem[filename], builtins) | 
 | 293 | 	} else { | 
 | 294 | 		result, err = starlark.ExecFile(mainThread, filepath.Join(topDir, filename), nil, builtins) | 
 | 295 | 	} | 
 | 296 | 	return result, sortedStringKeys(moduleCache), err | 
 | 297 | } | 
 | 298 |  | 
 | 299 | func sortedStringKeys(m map[string]*modentry) []string { | 
 | 300 | 	s := make([]string, 0, len(m)) | 
 | 301 | 	for k := range m { | 
 | 302 | 		s = append(s, k) | 
 | 303 | 	} | 
 | 304 | 	sort.Strings(s) | 
 | 305 | 	return s | 
 | 306 | } |