newt/config/config.go (159 lines of code) (raw):
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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.
*/
// The config package handles reading of newt YAML files.
package config
import (
"io/ioutil"
"path/filepath"
"sort"
"strings"
log "github.com/sirupsen/logrus"
"github.com/spf13/cast"
"mynewt.apache.org/newt/newt/interfaces"
"mynewt.apache.org/newt/newt/newtutil"
"mynewt.apache.org/newt/newt/ycfg"
"mynewt.apache.org/newt/util"
"mynewt.apache.org/newt/yaml"
)
const (
KEYWORD_IMPORT = "$import"
)
// keywordMap is a map of all supported keywords. Config keywords always start
// with "$".
var keywordMap = map[string]struct{}{
KEYWORD_IMPORT: struct{}{},
}
// FileEntry represents a single YAML file. It does not contain import
// information.
type FileEntry struct {
FileInfo *util.FileInfo
Settings map[string]interface{}
}
func readSettings(path string) (map[string]interface{}, error) {
file, err := ioutil.ReadFile(path)
if err != nil {
return nil, util.ChildNewtError(err)
}
settings := map[string]interface{}{}
if err := yaml.Unmarshal(file, &settings); err != nil {
return nil, util.FmtNewtError("Failure parsing \"%s\": %s",
path, err.Error())
}
return settings, nil
}
func readFileEntry(path string, parent *util.FileInfo) (FileEntry, error) {
absPath, err := filepath.Abs(path)
if err != nil {
return FileEntry{}, err
}
settings, err := readSettings(absPath)
if err != nil {
return FileEntry{}, err
}
return FileEntry{
FileInfo: &util.FileInfo{
Path: absPath,
Parent: parent,
},
Settings: settings,
}, nil
}
func extractImports(settings map[string]interface{}) ([]string, error) {
itf := settings[KEYWORD_IMPORT]
if itf == nil {
return nil, nil
}
strs, err := cast.ToStringSliceE(itf)
if err != nil {
return nil, util.FmtNewtError(
"invalid %s section; must contain sequence of strings",
KEYWORD_IMPORT)
}
return strs, nil
}
func (fe *FileEntry) warnUnrecognizedKeywords() {
m := map[string]struct{}{}
// Find all unrecognized entries starting with "$".
for k, _ := range fe.Settings {
if strings.HasPrefix(k, "$") {
if _, ok := keywordMap[k]; !ok {
m[k] = struct{}{}
}
}
}
if len(m) == 0 {
return
}
keywords := make([]string, 0, len(m))
for k, _ := range m {
keywords = append(keywords, k)
}
sort.Strings(keywords)
s := ""
for _, k := range keywords {
s += "\n " + k
}
util.OneTimeWarning(
"%s contains unrecognized keywords: %s\n"+
"you may need to upgrade your version of newt.",
fe.FileInfo.Path, s)
}
// readLineage reads a configuration file and all files it imports (directly
// or indirectly). The resulting []FileEntry is sorted in the order the
// corresponding files were read.
func readLineage(path string) ([]FileEntry, error) {
entries := []FileEntry{}
seen := map[string]struct{}{}
// Recursively process imports, accumulating file info in `entries`.
var iter func(path string, parent *util.FileInfo) error
iter = func(path string, parent *util.FileInfo) error {
// Relative paths are relative to the project base.
if !filepath.IsAbs(path) {
proj := interfaces.GetProject()
newPath, err := proj.ResolvePath(proj.Path(), path)
if err != nil {
return err
}
path = newPath
}
// Only operate on absolute paths to ensure each file has a unique ID.
absPath, err := filepath.Abs(path)
if err != nil {
return parent.ErrTree(err)
}
// Don't process the same config file twice.
if _, ok := seen[absPath]; ok {
return nil
}
seen[absPath] = struct{}{}
entry, err := readFileEntry(path, parent)
if err != nil {
return parent.ErrTree(err)
}
imports, err := extractImports(entry.Settings)
if err != nil {
return err
}
for _, imp := range imports {
if err := iter(imp, entry.FileInfo); err != nil {
return err
}
}
// Only add the top-level entry now that the imports have been
// processed. This comes last so that it can override settings
// specified by imported files.
entries = append(entries, entry)
return nil
}
// Recursively read imported configuration files.
if err := iter(path, nil); err != nil {
return nil, err
}
// Log the configuration files that were read. If there are imports, log
// it using a tree notation.
if len(entries) == 1 {
log.Debugf("Read config file: %s",
newtutil.ProjRelPath(entries[0].FileInfo.Path))
} else {
tree, err := BuildTree(entries)
if err != nil {
return nil, err
}
log.Debugf("Read config files:\n%s", TreeString(tree))
}
return entries, nil
}
// ReadFile reads a YAML file, processes all its `$import` directives, and
// returns a populated YCfg tree.
func ReadFile(path string) (ycfg.YCfg, error) {
yc := ycfg.NewYCfg(path)
entries, err := readLineage(path)
if err != nil {
return yc, err
}
for _, e := range entries {
for k, v := range e.Settings {
if err := yc.MergeFromFile(k, v, e.FileInfo); err != nil {
return yc, e.FileInfo.Parent.ErrTree(err)
}
}
}
return yc, nil
}