blob: 462ea9928b51f4631a3c4ac0f5bfcdff751409e2 [file] [log] [blame]
// 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.
//
// All Starlark code is executed sequentially in a single goroutine from inside
// Generate function, thus this package doesn't used any mutexes or other
// synchronization primitives. It is safe to call Generate concurrently though,
// since there's no global shared state, each Generate call operates on its
// own state.
package lucicfg
import (
"context"
"fmt"
"io/fs"
"sort"
"go.starlark.net/lib/json"
"go.starlark.net/starlark"
"go.chromium.org/luci/starlark/builtins"
"go.chromium.org/luci/starlark/interpreter"
"go.chromium.org/luci/starlark/starlarkproto"
embedded "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
Path string // absolute path to the main package, if known
Entry string // a name of the entry point script in this package
Meta *Meta // defaults for lucicfg own parameters
Vars map[string]string // var values passed via `-var key=value` flags
// Used to setup additional facilities for unit tests.
testOmitHeader bool
testPredeclared starlark.StringDict
testThreadModifier func(th *starlark.Thread)
testDisableFailureCollector bool
testVersion string
}
// 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 := NewState(in)
ctx = withState(ctx, state)
// Do not put frequently changing version string into test outputs.
ver := Version
if in.testVersion != "" {
ver = in.testVersion
}
// All available symbols 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,
"json": json.Module,
"to_json": toSortedJSON, // see json.go, deprecated
// '__native__' is NOT public API. It should be used only through public
// @stdlib functions.
"__native__": native(starlark.StringDict{
// How the generator was launched.
"version": versionTuple(ver),
"entry_point": starlark.String(in.Entry),
"var_flags": asFrozenDict(in.Vars),
"running_tests": starlark.Bool(in.testThreadModifier != nil),
// Some built-in utilities implemented in `builtins` package.
"ctor": builtins.Ctor,
"genstruct": builtins.GenStruct,
"re_submatches": builtins.RegexpMatcher("submatches"),
// Built-in proto descriptors.
"wellknown_descpb": wellKnownDescSet,
"googtypes_descpb": googTypesDescSet,
"annotations_descpb": annotationsDescSet,
"validation_descpb": validateDescSet,
"lucitypes_descpb": luciTypesDescSet,
}),
}
for k, v := range in.testPredeclared {
predeclared[k] = v
}
// Expose @stdlib and __main__ package. They 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
// Create a proto loader, hook up load("@proto//<path>", ...) to load proto
// modules through it. See ThreadModifier below where it is set as default in
// the thread. This exposes it to Starlark code, so it can register descriptor
// sets in it.
ploader := starlarkproto.NewLoader()
pkgs["proto"] = func(path string) (dict starlark.StringDict, _ string, err error) {
mod, err := ploader.Module(path)
if err != nil {
return nil, "", err
}
return starlark.StringDict{mod.Name: mod}, "", nil
}
// Capture details of fail(...) calls happening inside Starlark code.
failures := builtins.FailureCollector{}
// 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,
PreExec: func(th *starlark.Thread, _ interpreter.ModuleKey) { state.vars.OpenScope(th) },
PostExec: func(th *starlark.Thread, _ interpreter.ModuleKey) { state.vars.CloseScope(th) },
ThreadModifier: func(th *starlark.Thread) {
starlarkproto.SetDefaultLoader(th, ploader)
starlarkproto.SetMessageCache(th, &state.protos)
if !in.testDisableFailureCollector {
failures.Install(th)
}
if in.testThreadModifier != nil {
in.testThreadModifier(th)
}
},
}
// Load builtins.star, and then execute the user-supplied script.
var err error
if err = intr.Init(ctx); err == nil {
_, err = intr.ExecModule(ctx, interpreter.MainPkg, in.Entry)
}
if err != nil {
if f := failures.LatestFailure(); f != nil {
err = f // prefer this error, it has custom stack trace
}
return nil, state.err(err)
}
// Verify all var values provided via Inputs.Vars were actually used by
// lucicfg.var(expose_as='...') definitions.
if errs := state.checkUnconsumedVars(); len(errs) != 0 {
return nil, state.err(errs...)
}
// Executing the script (with all its dependencies) populated the graph.
// Finalize it. This checks there are no dangling edges, freezes the graph,
// and makes it queryable, so generator callbacks can traverse it.
if errs := state.graph.Finalize(); len(errs) != 0 {
return nil, state.err(errs...)
}
// The script registered a bunch of callbacks that take the graph and
// transform it into actual output config files. Run these callbacks now.
genCtx := newGenCtx()
if errs := state.generators.call(intr.Thread(ctx), genCtx); len(errs) != 0 {
return nil, state.err(errs...)
}
output, err := genCtx.assembleOutput(!in.testOmitHeader)
if err != nil {
return nil, state.err(err)
}
state.Output = output
if len(state.errors) != 0 {
return nil, state.errors
}
// Discover what main package modules we actually executed.
for _, key := range intr.Visited() {
if key.Package == interpreter.MainPkg {
state.Visited = append(state.Visited, key.Path)
}
}
return state, nil
}
// embeddedPackages makes a map of loaders for embedded Starlark packages.
//
// A 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 {
out := make(map[string]interpreter.Loader, 1)
for _, pkg := range []string{"stdlib"} {
sub, err := fs.Sub(embedded.Content, pkg)
if err != nil {
panic(fmt.Sprintf("%q is not embedded", pkg))
}
out[pkg] = interpreter.FSLoader(sub)
}
return out
}
// asFrozenDict converts a map to a frozen Starlark dict.
func asFrozenDict(m map[string]string) *starlark.Dict {
keys := make([]string, 0, len(m))
for k := range m {
keys = append(keys, k)
}
sort.Strings(keys)
d := starlark.NewDict(len(m))
for _, k := range keys {
d.SetKey(starlark.String(k), starlark.String(m[k]))
}
d.Freeze()
return d
}