| // Copyright 2019 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 |
| |
| import ( |
| "context" |
| "flag" |
| "fmt" |
| "path/filepath" |
| "strings" |
| |
| "go.starlark.net/starlark" |
| |
| luciflag "go.chromium.org/luci/common/flag" |
| "go.chromium.org/luci/common/logging" |
| ) |
| |
| // Meta contains configuration for the configuration generator itself. |
| // |
| // It influences how generator produces output configs. It is settable through |
| // lucicfg.config(...) statements on the Starlark side or through command line |
| // flags. Command line flags override what was set via lucicfg.config(...). |
| // |
| // See @stdlib//internal/lucicfg.star for full meaning of fields. |
| type Meta struct { |
| ConfigServiceHost string `json:"config_service_host"` // LUCI config host name |
| ConfigDir string `json:"config_dir"` // output directory to place generated files or '-' for stdout |
| TrackedFiles []string `json:"tracked_files"` // e.g. ["*.cfg", "!*-dev.cfg"] |
| FailOnWarnings bool `json:"fail_on_warnings"` // true to treat validation warnings as errors |
| LintChecks []string `json:"lint_checks"` // active lint checks |
| |
| // FlagSet passed to AddFlags and AddOutputFlags. |
| fs *flag.FlagSet |
| // Pointers to fields that were touched. Used when merging two Metas together. |
| touched map[interface{}]struct{} |
| // True if detectTouchedFlags() was already called. |
| detectedTouchedFlags bool |
| } |
| |
| // Copy returns an "untouched" copy of `m`. |
| // |
| // In the returned copy WasTouched reports all fields as untouched. |
| func (m *Meta) Copy() Meta { |
| if m == nil { |
| return Meta{} |
| } |
| return Meta{ |
| ConfigServiceHost: m.ConfigServiceHost, |
| ConfigDir: m.ConfigDir, |
| TrackedFiles: append([]string(nil), m.TrackedFiles...), |
| FailOnWarnings: m.FailOnWarnings, |
| LintChecks: append([]string(nil), m.LintChecks...), |
| } |
| } |
| |
| // Log logs the values of the meta parameters to Debug logger. |
| func (m *Meta) Log(ctx context.Context) { |
| logging.Debugf(ctx, "Meta config:") |
| logging.Debugf(ctx, " config_service_host = %q", m.ConfigServiceHost) |
| logging.Debugf(ctx, " config_dir = %q", m.ConfigDir) |
| logging.Debugf(ctx, " tracked_files = %v", m.TrackedFiles) |
| logging.Debugf(ctx, " fail_on_warnings = %v", m.FailOnWarnings) |
| logging.Debugf(ctx, " lint_checks = %v", m.LintChecks) |
| } |
| |
| // RebaseConfigDir changes ConfigDir, if it is set, to be absolute by appending |
| // it to the given root. |
| // |
| // Doesn't touch "-", which indicates "stdout". |
| func (m *Meta) RebaseConfigDir(root string) { |
| if m.ConfigDir != "" && m.ConfigDir != "-" { |
| m.ConfigDir = filepath.Join(root, filepath.FromSlash(m.ConfigDir)) |
| } |
| } |
| |
| // AddFlags registers command line flags that correspond to Meta fields. |
| func (m *Meta) AddFlags(fs *flag.FlagSet) { |
| m.fs = fs |
| fs.StringVar(&m.ConfigServiceHost, "config-service-host", m.ConfigServiceHost, "Hostname of a LUCI config service to use for validation.") |
| fs.StringVar(&m.ConfigDir, "config-dir", m.ConfigDir, |
| `A directory to place generated configs into (relative to cwd if given as a |
| flag otherwise relative to the main script). If '-', generated configs are just |
| printed to stdout in a format useful for debugging.`) |
| fs.Var(luciflag.CommaList(&m.TrackedFiles), "tracked-files", "Globs for files considered generated. See lucicfg.config(...) doc for more info.") |
| fs.BoolVar(&m.FailOnWarnings, "fail-on-warnings", m.FailOnWarnings, "Treat validation warnings as errors.") |
| fs.Var(luciflag.CommaList(&m.LintChecks), "lint-checks", "When validating, apply these lint checks. See lucicfg.config(...) doc for more info.") |
| } |
| |
| // detectTouchedFlags is called after flags are parsed to figure out what flags |
| // were explicitly set and what were left at their defaults. |
| // |
| // It updates Meta with information about touched flags which is later used |
| // by PopulateFromTouchedIn and WasTouched functions. |
| func (m *Meta) detectTouchedFlags() { |
| switch { |
| case m.fs == nil: |
| return // not using CLI flags at all |
| case m.detectedTouchedFlags: |
| return // already did this |
| case !m.fs.Parsed(): |
| panic("detectTouchedFlags should be called after flags are parsed") |
| } |
| m.detectedTouchedFlags = true |
| |
| fields := m.fieldsMap() |
| |
| m.fs.Visit(func(f *flag.Flag) { |
| if ptr := fields[strings.Replace(f.Name, "-", "_", -1)]; ptr != nil { |
| m.touch(ptr) |
| } |
| }) |
| } |
| |
| // PopulateFromTouchedIn takes all touched values in `t` and copies them to |
| // `m`, overriding what's in `m`. |
| func (m *Meta) PopulateFromTouchedIn(t *Meta) { |
| t.detectTouchedFlags() |
| left := m.fieldsMap() |
| right := t.fieldsMap() |
| for k, l := range left { |
| r := right[k] |
| if _, yes := t.touched[r]; yes { |
| // Do *l = *r. |
| switch l.(type) { |
| case *string: |
| *(l.(*string)) = *(r.(*string)) |
| case *bool: |
| *(l.(*bool)) = *(r.(*bool)) |
| case *[]string: |
| *(l.(*[]string)) = append([]string(nil), *(r.(*[]string))...) |
| default: |
| panic("impossible") |
| } |
| } |
| } |
| } |
| |
| // WasTouched returns true if the field (given by its Starlark snake_case name) |
| // was explicitly set via CLI flags or via lucicfg.config(...) in Starlark. |
| // |
| // Panics if the field is unrecognized. |
| func (m *Meta) WasTouched(field string) bool { |
| m.detectTouchedFlags() |
| ptr, ok := m.fieldsMap()[field] |
| if !ok { |
| panic(fmt.Sprintf("no such meta field %s", field)) |
| } |
| _, yes := m.touched[ptr] |
| return yes |
| } |
| |
| //////////////////////////////////////////////////////////////////////////////// |
| |
| // touch takes a pointer to some Meta field and marks it as explicitly set. |
| func (m *Meta) touch(ptr interface{}) { |
| if m.touched == nil { |
| m.touched = make(map[interface{}]struct{}, 1) |
| } |
| m.touched[ptr] = struct{}{} |
| } |
| |
| // fieldsMap returns a mapping from a snake_case name of a field to a pointer to |
| // this field inside 'm'. |
| // |
| // This is used by both Starlark accessors and for processing CLI flags. |
| func (m *Meta) fieldsMap() map[string]interface{} { |
| return map[string]interface{}{ |
| "config_service_host": &m.ConfigServiceHost, |
| "config_dir": &m.ConfigDir, |
| "tracked_files": &m.TrackedFiles, |
| "fail_on_warnings": &m.FailOnWarnings, |
| "lint_checks": &m.LintChecks, |
| } |
| } |
| |
| // getField gets the field k. |
| func (m *Meta) getField(k string) (starlark.Value, error) { |
| ptr := m.fieldsMap()[k] |
| if ptr == nil { |
| return nil, fmt.Errorf("get_meta: no such meta key %q", k) |
| } |
| |
| switch ptr := ptr.(type) { |
| case *string: |
| return starlark.String(*ptr), nil |
| |
| case *bool: |
| return starlark.Bool(*ptr), nil |
| |
| case *[]string: |
| vals := make([]starlark.Value, len(*ptr)) |
| for idx, str := range *ptr { |
| vals[idx] = starlark.String(str) |
| } |
| return starlark.NewList(vals), nil |
| |
| default: |
| panic("impossible") |
| } |
| } |
| |
| // setField sets the field k to v. |
| func (m *Meta) setField(k string, v starlark.Value) (err error) { |
| ptr := m.fieldsMap()[k] |
| if ptr == nil { |
| return fmt.Errorf("set_meta: no such meta key %q", k) |
| } |
| |
| // On success, mark the field as modified. |
| defer func() { |
| if err == nil { |
| m.touch(ptr) |
| } |
| }() |
| |
| switch ptr := ptr.(type) { |
| case *string: |
| if str, ok := v.(starlark.String); ok { |
| *ptr = str.GoString() |
| return nil |
| } |
| return fmt.Errorf("set_meta: got %s, expecting string", v.Type()) |
| |
| case *bool: |
| if b, ok := v.(starlark.Bool); ok { |
| *ptr = bool(b) |
| return nil |
| } |
| return fmt.Errorf("set_meta: got %s, expecting bool", v.Type()) |
| |
| case *[]string: |
| if iterable, ok := v.(starlark.Iterable); ok { |
| iter := iterable.Iterate() |
| defer iter.Done() |
| |
| var vals []string |
| for x := starlark.Value(nil); iter.Next(&x); { |
| if str, ok := x.(starlark.String); ok { |
| vals = append(vals, str.GoString()) |
| } else { |
| return fmt.Errorf("set_meta: got %s, expecting string", x.Type()) |
| } |
| } |
| |
| *ptr = vals |
| return nil |
| } |
| return fmt.Errorf("set_meta: got %s, expecting an iterable", v.Type()) |
| |
| default: |
| panic("impossible") |
| } |
| } |
| |
| func init() { |
| // get_meta(k) returns the value of the corresponding field in Meta. |
| declNative("get_meta", func(call nativeCall) (starlark.Value, error) { |
| var k starlark.String |
| if err := call.unpack(1, &k); err != nil { |
| return nil, err |
| } |
| return call.State.Meta.getField(k.GoString()) |
| }) |
| |
| // set_meta(k, v) sets the value of the corresponding field in Meta. |
| declNative("set_meta", func(call nativeCall) (starlark.Value, error) { |
| var k starlark.String |
| var v starlark.Value |
| if err := call.unpack(2, &k, &v); err != nil { |
| return nil, err |
| } |
| return starlark.None, call.State.Meta.setField(k.GoString(), v) |
| }) |
| } |