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 }