|
@@ -0,0 +1,306 @@
|
|
|
+// Copyright 2023 Google Inc. All rights reserved.
|
|
|
+//
|
|
|
+// Licensed under the Apache License, Version 2.0 (the "License");
|
|
|
+// you may not use this file except in compliance with the License.
|
|
|
+// You may obtain a copy of the License at
|
|
|
+//
|
|
|
+// http://www.apache.org/licenses/LICENSE-2.0
|
|
|
+//
|
|
|
+// Unless required by applicable law or agreed to in writing, software
|
|
|
+// distributed under the License is distributed on an "AS IS" BASIS,
|
|
|
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
|
+// See the License for the specific language governing permissions and
|
|
|
+// limitations under the License.
|
|
|
+
|
|
|
+package starlark_import
|
|
|
+
|
|
|
+import (
|
|
|
+ "fmt"
|
|
|
+ "os"
|
|
|
+ "path/filepath"
|
|
|
+ "sort"
|
|
|
+ "strings"
|
|
|
+ "sync"
|
|
|
+ "time"
|
|
|
+
|
|
|
+ "go.starlark.net/starlark"
|
|
|
+ "go.starlark.net/starlarkjson"
|
|
|
+ "go.starlark.net/starlarkstruct"
|
|
|
+)
|
|
|
+
|
|
|
+func init() {
|
|
|
+ go func() {
|
|
|
+ startTime := time.Now()
|
|
|
+ v, d, err := runStarlarkFile("//build/bazel/constants_exported_to_soong.bzl")
|
|
|
+ endTime := time.Now()
|
|
|
+ //fmt.Fprintf(os.Stderr, "starlark run time: %s\n", endTime.Sub(startTime).String())
|
|
|
+ globalResult.Set(starlarkResult{
|
|
|
+ values: v,
|
|
|
+ ninjaDeps: d,
|
|
|
+ err: err,
|
|
|
+ startTime: startTime,
|
|
|
+ endTime: endTime,
|
|
|
+ })
|
|
|
+ }()
|
|
|
+}
|
|
|
+
|
|
|
+type starlarkResult struct {
|
|
|
+ values starlark.StringDict
|
|
|
+ ninjaDeps []string
|
|
|
+ err error
|
|
|
+ startTime time.Time
|
|
|
+ endTime time.Time
|
|
|
+}
|
|
|
+
|
|
|
+// setOnce wraps a value and exposes Set() and Get() accessors for it.
|
|
|
+// The Get() calls will block until a Set() has been called.
|
|
|
+// A second call to Set() will panic.
|
|
|
+// setOnce must be created using newSetOnce()
|
|
|
+type setOnce[T any] struct {
|
|
|
+ value T
|
|
|
+ lock sync.Mutex
|
|
|
+ wg sync.WaitGroup
|
|
|
+ isSet bool
|
|
|
+}
|
|
|
+
|
|
|
+func (o *setOnce[T]) Set(value T) {
|
|
|
+ o.lock.Lock()
|
|
|
+ defer o.lock.Unlock()
|
|
|
+ if o.isSet {
|
|
|
+ panic("Value already set")
|
|
|
+ }
|
|
|
+
|
|
|
+ o.value = value
|
|
|
+ o.isSet = true
|
|
|
+ o.wg.Done()
|
|
|
+}
|
|
|
+
|
|
|
+func (o *setOnce[T]) Get() T {
|
|
|
+ if !o.isSet {
|
|
|
+ o.wg.Wait()
|
|
|
+ }
|
|
|
+ return o.value
|
|
|
+}
|
|
|
+
|
|
|
+func newSetOnce[T any]() *setOnce[T] {
|
|
|
+ result := &setOnce[T]{}
|
|
|
+ result.wg.Add(1)
|
|
|
+ return result
|
|
|
+}
|
|
|
+
|
|
|
+var globalResult = newSetOnce[starlarkResult]()
|
|
|
+
|
|
|
+func GetStarlarkValue[T any](key string) (T, error) {
|
|
|
+ result := globalResult.Get()
|
|
|
+ if result.err != nil {
|
|
|
+ var zero T
|
|
|
+ return zero, result.err
|
|
|
+ }
|
|
|
+ if !result.values.Has(key) {
|
|
|
+ var zero T
|
|
|
+ return zero, fmt.Errorf("a starlark variable by that name wasn't found, did you update //build/bazel/constants_exported_to_soong.bzl?")
|
|
|
+ }
|
|
|
+ return Unmarshal[T](result.values[key])
|
|
|
+}
|
|
|
+
|
|
|
+func GetNinjaDeps() ([]string, error) {
|
|
|
+ result := globalResult.Get()
|
|
|
+ if result.err != nil {
|
|
|
+ return nil, result.err
|
|
|
+ }
|
|
|
+ return result.ninjaDeps, nil
|
|
|
+}
|
|
|
+
|
|
|
+func getTopDir() (string, error) {
|
|
|
+ // It's hard to communicate the top dir to this package in any other way than reading the
|
|
|
+ // arguments directly, because we need to know this at package initialization time. Many
|
|
|
+ // soong constants that we'd like to read from starlark are initialized during package
|
|
|
+ // initialization.
|
|
|
+ for i, arg := range os.Args {
|
|
|
+ if arg == "--top" {
|
|
|
+ if i < len(os.Args)-1 && os.Args[i+1] != "" {
|
|
|
+ return os.Args[i+1], nil
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // When running tests, --top is not passed. Instead, search for the top dir manually
|
|
|
+ cwd, err := os.Getwd()
|
|
|
+ if err != nil {
|
|
|
+ return "", err
|
|
|
+ }
|
|
|
+ for cwd != "/" {
|
|
|
+ if _, err := os.Stat(filepath.Join(cwd, "build/soong/soong_ui.bash")); err == nil {
|
|
|
+ return cwd, nil
|
|
|
+ }
|
|
|
+ cwd = filepath.Dir(cwd)
|
|
|
+ }
|
|
|
+ return "", fmt.Errorf("could not find top dir")
|
|
|
+}
|
|
|
+
|
|
|
+const callerDirKey = "callerDir"
|
|
|
+
|
|
|
+type modentry struct {
|
|
|
+ globals starlark.StringDict
|
|
|
+ err error
|
|
|
+}
|
|
|
+
|
|
|
+func unsupportedMethod(t *starlark.Thread, fn *starlark.Builtin, _ starlark.Tuple, _ []starlark.Tuple) (starlark.Value, error) {
|
|
|
+ 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())
|
|
|
+}
|
|
|
+
|
|
|
+var builtins = starlark.StringDict{
|
|
|
+ "aspect": starlark.NewBuiltin("aspect", unsupportedMethod),
|
|
|
+ "glob": starlark.NewBuiltin("glob", unsupportedMethod),
|
|
|
+ "json": starlarkjson.Module,
|
|
|
+ "provider": starlark.NewBuiltin("provider", unsupportedMethod),
|
|
|
+ "rule": starlark.NewBuiltin("rule", unsupportedMethod),
|
|
|
+ "struct": starlark.NewBuiltin("struct", starlarkstruct.Make),
|
|
|
+ "select": starlark.NewBuiltin("select", unsupportedMethod),
|
|
|
+ "transition": starlark.NewBuiltin("transition", unsupportedMethod),
|
|
|
+}
|
|
|
+
|
|
|
+// Takes a module name (the first argument to the load() function) and returns the path
|
|
|
+// it's trying to load, stripping out leading //, and handling leading :s.
|
|
|
+func cleanModuleName(moduleName string, callerDir string) (string, error) {
|
|
|
+ if strings.Count(moduleName, ":") > 1 {
|
|
|
+ return "", fmt.Errorf("at most 1 colon must be present in starlark path: %s", moduleName)
|
|
|
+ }
|
|
|
+
|
|
|
+ // We don't have full support for external repositories, but at least support skylib's dicts.
|
|
|
+ if moduleName == "@bazel_skylib//lib:dicts.bzl" {
|
|
|
+ return "external/bazel-skylib/lib/dicts.bzl", nil
|
|
|
+ }
|
|
|
+
|
|
|
+ localLoad := false
|
|
|
+ if strings.HasPrefix(moduleName, "@//") {
|
|
|
+ moduleName = moduleName[3:]
|
|
|
+ } else if strings.HasPrefix(moduleName, "//") {
|
|
|
+ moduleName = moduleName[2:]
|
|
|
+ } else if strings.HasPrefix(moduleName, ":") {
|
|
|
+ moduleName = moduleName[1:]
|
|
|
+ localLoad = true
|
|
|
+ } else {
|
|
|
+ return "", fmt.Errorf("load path must start with // or :")
|
|
|
+ }
|
|
|
+
|
|
|
+ if ix := strings.LastIndex(moduleName, ":"); ix >= 0 {
|
|
|
+ moduleName = moduleName[:ix] + string(os.PathSeparator) + moduleName[ix+1:]
|
|
|
+ }
|
|
|
+
|
|
|
+ if filepath.Clean(moduleName) != moduleName {
|
|
|
+ return "", fmt.Errorf("load path must be clean, found: %s, expected: %s", moduleName, filepath.Clean(moduleName))
|
|
|
+ }
|
|
|
+ if strings.HasPrefix(moduleName, "../") {
|
|
|
+ return "", fmt.Errorf("load path must not start with ../: %s", moduleName)
|
|
|
+ }
|
|
|
+ if strings.HasPrefix(moduleName, "/") {
|
|
|
+ return "", fmt.Errorf("load path starts with /, use // for a absolute path: %s", moduleName)
|
|
|
+ }
|
|
|
+
|
|
|
+ if localLoad {
|
|
|
+ return filepath.Join(callerDir, moduleName), nil
|
|
|
+ }
|
|
|
+
|
|
|
+ return moduleName, nil
|
|
|
+}
|
|
|
+
|
|
|
+// loader implements load statement. The format of the loaded module URI is
|
|
|
+//
|
|
|
+// [//path]:base
|
|
|
+//
|
|
|
+// The file path is $ROOT/path/base if path is present, <caller_dir>/base otherwise.
|
|
|
+func loader(thread *starlark.Thread, module string, topDir string, moduleCache map[string]*modentry, moduleCacheLock *sync.Mutex, filesystem map[string]string) (starlark.StringDict, error) {
|
|
|
+ modulePath, err := cleanModuleName(module, thread.Local(callerDirKey).(string))
|
|
|
+ if err != nil {
|
|
|
+ return nil, err
|
|
|
+ }
|
|
|
+ moduleCacheLock.Lock()
|
|
|
+ e, ok := moduleCache[modulePath]
|
|
|
+ if e == nil {
|
|
|
+ if ok {
|
|
|
+ moduleCacheLock.Unlock()
|
|
|
+ return nil, fmt.Errorf("cycle in load graph")
|
|
|
+ }
|
|
|
+
|
|
|
+ // Add a placeholder to indicate "load in progress".
|
|
|
+ moduleCache[modulePath] = nil
|
|
|
+ moduleCacheLock.Unlock()
|
|
|
+
|
|
|
+ childThread := &starlark.Thread{Name: "exec " + module, Load: thread.Load}
|
|
|
+
|
|
|
+ // Cheating for the sake of testing:
|
|
|
+ // propagate starlarktest's Reporter key, otherwise testing
|
|
|
+ // the load function may cause panic in starlarktest code.
|
|
|
+ const testReporterKey = "Reporter"
|
|
|
+ if v := thread.Local(testReporterKey); v != nil {
|
|
|
+ childThread.SetLocal(testReporterKey, v)
|
|
|
+ }
|
|
|
+
|
|
|
+ childThread.SetLocal(callerDirKey, filepath.Dir(modulePath))
|
|
|
+
|
|
|
+ if filesystem != nil {
|
|
|
+ globals, err := starlark.ExecFile(childThread, filepath.Join(topDir, modulePath), filesystem[modulePath], builtins)
|
|
|
+ e = &modentry{globals, err}
|
|
|
+ } else {
|
|
|
+ globals, err := starlark.ExecFile(childThread, filepath.Join(topDir, modulePath), nil, builtins)
|
|
|
+ e = &modentry{globals, err}
|
|
|
+ }
|
|
|
+
|
|
|
+ // Update the cache.
|
|
|
+ moduleCacheLock.Lock()
|
|
|
+ moduleCache[modulePath] = e
|
|
|
+ }
|
|
|
+ moduleCacheLock.Unlock()
|
|
|
+ return e.globals, e.err
|
|
|
+}
|
|
|
+
|
|
|
+// Run runs the given starlark file and returns its global variables and a list of all starlark
|
|
|
+// files that were loaded. The top dir for starlark's // is found via getTopDir().
|
|
|
+func runStarlarkFile(filename string) (starlark.StringDict, []string, error) {
|
|
|
+ topDir, err := getTopDir()
|
|
|
+ if err != nil {
|
|
|
+ return nil, nil, err
|
|
|
+ }
|
|
|
+ return runStarlarkFileWithFilesystem(filename, topDir, nil)
|
|
|
+}
|
|
|
+
|
|
|
+func runStarlarkFileWithFilesystem(filename string, topDir string, filesystem map[string]string) (starlark.StringDict, []string, error) {
|
|
|
+ if !strings.HasPrefix(filename, "//") && !strings.HasPrefix(filename, ":") {
|
|
|
+ filename = "//" + filename
|
|
|
+ }
|
|
|
+ filename, err := cleanModuleName(filename, "")
|
|
|
+ if err != nil {
|
|
|
+ return nil, nil, err
|
|
|
+ }
|
|
|
+ moduleCache := make(map[string]*modentry)
|
|
|
+ moduleCache[filename] = nil
|
|
|
+ moduleCacheLock := &sync.Mutex{}
|
|
|
+ mainThread := &starlark.Thread{
|
|
|
+ Name: "main",
|
|
|
+ Print: func(_ *starlark.Thread, msg string) {
|
|
|
+ // Ignore prints
|
|
|
+ },
|
|
|
+ Load: func(thread *starlark.Thread, module string) (starlark.StringDict, error) {
|
|
|
+ return loader(thread, module, topDir, moduleCache, moduleCacheLock, filesystem)
|
|
|
+ },
|
|
|
+ }
|
|
|
+ mainThread.SetLocal(callerDirKey, filepath.Dir(filename))
|
|
|
+
|
|
|
+ var result starlark.StringDict
|
|
|
+ if filesystem != nil {
|
|
|
+ result, err = starlark.ExecFile(mainThread, filepath.Join(topDir, filename), filesystem[filename], builtins)
|
|
|
+ } else {
|
|
|
+ result, err = starlark.ExecFile(mainThread, filepath.Join(topDir, filename), nil, builtins)
|
|
|
+ }
|
|
|
+ return result, sortedStringKeys(moduleCache), err
|
|
|
+}
|
|
|
+
|
|
|
+func sortedStringKeys(m map[string]*modentry) []string {
|
|
|
+ s := make([]string, 0, len(m))
|
|
|
+ for k := range m {
|
|
|
+ s = append(s, k)
|
|
|
+ }
|
|
|
+ sort.Strings(s)
|
|
|
+ return s
|
|
|
+}
|