blob: ebe42472cd1ed6eaa1eed94655a3befbcedadc48 [file] [log] [blame]
Cole Faustc9508aa2023-02-07 11:38:27 -08001// 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
15package starlark_import
16
17import (
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
31func 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
47type 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()
59type setOnce[T any] struct {
60 value T
61 lock sync.Mutex
62 wg sync.WaitGroup
63 isSet bool
64}
65
66func (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
78func (o *setOnce[T]) Get() T {
79 if !o.isSet {
80 o.wg.Wait()
81 }
82 return o.value
83}
84
85func newSetOnce[T any]() *setOnce[T] {
86 result := &setOnce[T]{}
87 result.wg.Add(1)
88 return result
89}
90
91var globalResult = newSetOnce[starlarkResult]()
92
93func 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
106func 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
114func 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
141const callerDirKey = "callerDir"
142
143type modentry struct {
144 globals starlark.StringDict
145 err error
146}
147
148func 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
152var 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.
165func 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.
213func 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().
260func 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
268func 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
299func 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}