307 lines
9.1 KiB
Go
307 lines
9.1 KiB
Go
|
// 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
|
||
|
}
|