| // Copyright 2018 The LUCI Authors. |
| // |
| // 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 lucicfg contains LUCI config generator. |
| package lucicfg |
| |
| import ( |
| "context" |
| "fmt" |
| "strings" |
| |
| "go.starlark.net/starlark" |
| |
| "go.chromium.org/luci/starlark/builtins" |
| "go.chromium.org/luci/starlark/interpreter" |
| "go.chromium.org/luci/starlark/starlarkproto" |
| |
| generated "go.chromium.org/luci/lucicfg/starlark" |
| ) |
| |
| // Inputs define all inputs for the config generator. |
| type Inputs struct { |
| Code interpreter.Loader // a package with the user supplied code |
| Entry string // a name of the entry point script in this package |
| |
| TextPBHeader string // a header to put into generated textpb files |
| |
| // Used to setup additional facilities for unit tests. |
| testPredeclared starlark.StringDict |
| testThreadModified func(th *starlark.Thread) |
| } |
| |
| // Generate interprets the high-level config. |
| // |
| // Returns a multi-error with all captured errors. Some of them may implement |
| // BacktracableError interface. |
| func Generate(ctx context.Context, in Inputs) (*State, error) { |
| state := &State{Inputs: in} |
| ctx = withState(ctx, state) |
| |
| // All available functions implemented in go. |
| predeclared := starlark.StringDict{ |
| // Part of public API of the generator. |
| "fail": builtins.Fail, |
| "proto": starlarkproto.ProtoLib()["proto"], |
| "stacktrace": builtins.Stacktrace, |
| "struct": builtins.Struct, |
| "to_json": builtins.ToJSON, |
| |
| // '__native__' is NOT public API. It should be used only through public |
| // @stdlib functions. |
| "__native__": native(), |
| } |
| for k, v := range in.testPredeclared { |
| predeclared[k] = v |
| } |
| |
| // Expose @stdlib, @proto and __main__ package. All have no externally |
| // observable state of their own, but they call low-level __native__.* |
| // functions that manipulate 'state' by getting it through the context. |
| pkgs := embeddedPackages() |
| pkgs[interpreter.MainPkg] = in.Code |
| pkgs["proto"] = protoLoader() // see protos.go |
| |
| // Execute the config script in this environment. Return errors unwrapped so |
| // that callers can sniff out various sorts of Starlark errors. |
| intr := interpreter.Interpreter{ |
| Predeclared: predeclared, |
| Packages: pkgs, |
| ThreadModifier: in.testThreadModified, |
| } |
| if err := intr.Init(ctx); err != nil { |
| return nil, state.err(err) |
| } |
| if _, err := intr.LoadModule(ctx, interpreter.MainPkg, in.Entry); err != nil { |
| return nil, state.err(err) |
| } |
| |
| // Executing the script (with all its dependencies) populated the graph. |
| // It shouldn't be modified by any later stages of the execution. |
| state.graph.Freeze() |
| |
| // TODO(vadimsh): Check there are no dangling edges in the graph. |
| |
| // The script registered a bunch of callbacks that take the graph and |
| // transform it into actual config files (living in a config set). Run these |
| // callbacks now. |
| genCtx := newGenCtx() |
| if err := state.generators.call(intr.Thread(ctx), genCtx); err != nil { |
| return nil, state.err(err) |
| } |
| cfgs, err := genCtx.configSet.asTextProto(in.TextPBHeader) |
| if err != nil { |
| return nil, state.err(err) |
| } |
| state.Configs = cfgs |
| |
| if len(state.errors) != 0 { |
| return nil, state.errors |
| } |
| return state, nil |
| } |
| |
| // embeddedPackages makes a map of loaders for embedded Starlark packages. |
| // |
| // Each directory directly under go.chromium.org/luci/lucicfg/starlark/... |
| // represents a corresponding starlark package. E.g. files in 'stdlib' directory |
| // are loadable via load("@stdlib//<path>", ...). |
| func embeddedPackages() map[string]interpreter.Loader { |
| perRoot := map[string]map[string]string{} |
| |
| for path, data := range generated.Assets() { |
| chunks := strings.SplitN(path, "/", 2) |
| if len(chunks) != 2 { |
| panic(fmt.Sprintf("forbidden *.star outside the package dir: %s", path)) |
| } |
| root, rel := chunks[0], chunks[1] |
| m := perRoot[root] |
| if m == nil { |
| m = make(map[string]string, 1) |
| perRoot[root] = m |
| } |
| m[rel] = data |
| } |
| |
| loaders := make(map[string]interpreter.Loader, len(perRoot)) |
| for pkg, files := range perRoot { |
| loaders[pkg] = interpreter.MemoryLoader(files) |
| } |
| return loaders |
| } |