hugo/hugolib/config.go
Bjørn Erik Pedersen 923dd9d1c1
Fix config loading for "hugo mod init"
We have some commands that will continue even if the config loading fails (e.g. because a module can not be found). In Hugo 0.84.0 we introduced a new `_merge` flag that we removed once the configuration was loaded. But we did not do that in error situations, leading to failures in some situations.

This commit fixes that by making sure the configuration is always cleaned before return, even in error situations.

Fixes #8697
2021-06-27 12:03:43 +02:00

512 lines
14 KiB
Go

// Copyright 2019 The Hugo Authors. All rights reserved.
//
// 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 hugolib
import (
"os"
"path/filepath"
"strings"
"github.com/gohugoio/hugo/common/types"
"github.com/gohugoio/hugo/common/maps"
cpaths "github.com/gohugoio/hugo/common/paths"
"github.com/gobwas/glob"
hglob "github.com/gohugoio/hugo/hugofs/glob"
"github.com/gohugoio/hugo/common/loggers"
"github.com/gohugoio/hugo/cache/filecache"
"github.com/gohugoio/hugo/parser/metadecoders"
"github.com/gohugoio/hugo/common/herrors"
"github.com/gohugoio/hugo/common/hugo"
"github.com/gohugoio/hugo/hugolib/paths"
"github.com/gohugoio/hugo/langs"
"github.com/gohugoio/hugo/modules"
"github.com/pkg/errors"
"github.com/gohugoio/hugo/config"
"github.com/gohugoio/hugo/config/privacy"
"github.com/gohugoio/hugo/config/services"
"github.com/gohugoio/hugo/helpers"
"github.com/spf13/afero"
)
var ErrNoConfigFile = errors.New("Unable to locate config file or config directory. Perhaps you need to create a new site.\n Run `hugo help new` for details.\n")
// LoadConfig loads Hugo configuration into a new Viper and then adds
// a set of defaults.
func LoadConfig(d ConfigSourceDescriptor, doWithConfig ...func(cfg config.Provider) error) (config.Provider, []string, error) {
if d.Environment == "" {
d.Environment = hugo.EnvironmentProduction
}
if len(d.Environ) == 0 && !hugo.IsRunningAsTest() {
d.Environ = os.Environ()
}
var configFiles []string
l := configLoader{ConfigSourceDescriptor: d, cfg: config.New()}
// Make sure we always do this, even in error situations,
// as we have commands (e.g. "hugo mod init") that will
// use a partial configuration to do its job.
defer l.deleteMergeStrategies()
for _, name := range d.configFilenames() {
var filename string
filename, err := l.loadConfig(name)
if err == nil {
configFiles = append(configFiles, filename)
} else if err != ErrNoConfigFile {
return nil, nil, err
}
}
if err := l.applyConfigDefaults(); err != nil {
return l.cfg, configFiles, err
}
if d.AbsConfigDir != "" {
dcfg, dirnames, err := config.LoadConfigFromDir(l.Fs, d.AbsConfigDir, l.Environment)
if err == nil {
if len(dirnames) > 0 {
l.cfg.Set("", dcfg.Get(""))
configFiles = append(configFiles, dirnames...)
}
} else if err != ErrNoConfigFile {
if len(dirnames) > 0 {
return nil, nil, l.wrapFileError(err, dirnames[0])
}
return nil, nil, err
}
}
l.cfg.SetDefaultMergeStrategy()
// We create languages based on the settings, so we need to make sure that
// all configuration is loaded/set before doing that.
for _, d := range doWithConfig {
if err := d(l.cfg); err != nil {
return l.cfg, configFiles, err
}
}
// We made this a Glob pattern in Hugo 0.75, we don't need both.
if l.cfg.GetBool("ignoreVendor") {
helpers.Deprecated("--ignoreVendor", "--ignoreVendorPaths **", false)
l.cfg.Set("ignoreVendorPaths", "**")
}
// Some settings are used before we're done collecting all settings,
// so apply OS environment both before and after.
if err := l.applyOsEnvOverrides(d.Environ); err != nil {
return l.cfg, configFiles, err
}
modulesConfig, err := l.loadModulesConfig()
if err != nil {
return l.cfg, configFiles, err
}
// Need to run these after the modules are loaded, but before
// they are finalized.
collectHook := func(m *modules.ModulesConfig) error {
// We don't need the merge strategy configuration anymore,
// remove it so it doesn't accidentaly show up in other settings.
l.deleteMergeStrategies()
if err := l.loadLanguageSettings(nil); err != nil {
return err
}
mods := m.ActiveModules
// Apply default project mounts.
if err := modules.ApplyProjectConfigDefaults(l.cfg, mods[0]); err != nil {
return err
}
return nil
}
_, modulesConfigFiles, err := l.collectModules(modulesConfig, l.cfg, collectHook)
if err != nil {
return l.cfg, configFiles, err
}
configFiles = append(configFiles, modulesConfigFiles...)
if err := l.applyOsEnvOverrides(d.Environ); err != nil {
return l.cfg, configFiles, err
}
if err = l.applyConfigAliases(); err != nil {
return l.cfg, configFiles, err
}
return l.cfg, configFiles, err
}
// LoadConfigDefault is a convenience method to load the default "config.toml" config.
func LoadConfigDefault(fs afero.Fs) (config.Provider, error) {
v, _, err := LoadConfig(ConfigSourceDescriptor{Fs: fs, Filename: "config.toml"})
return v, err
}
// ConfigSourceDescriptor describes where to find the config (e.g. config.toml etc.).
type ConfigSourceDescriptor struct {
Fs afero.Fs
Logger loggers.Logger
// Path to the config file to use, e.g. /my/project/config.toml
Filename string
// The path to the directory to look for configuration. Is used if Filename is not
// set or if it is set to a relative filename.
Path string
// The project's working dir. Is used to look for additional theme config.
WorkingDir string
// The (optional) directory for additional configuration files.
AbsConfigDir string
// production, development
Environment string
// Defaults to os.Environ if not set.
Environ []string
}
func (d ConfigSourceDescriptor) configFileDir() string {
if d.Path != "" {
return d.Path
}
return d.WorkingDir
}
func (d ConfigSourceDescriptor) configFilenames() []string {
if d.Filename == "" {
return []string{"config"}
}
return strings.Split(d.Filename, ",")
}
// SiteConfig represents the config in .Site.Config.
type SiteConfig struct {
// This contains all privacy related settings that can be used to
// make the YouTube template etc. GDPR compliant.
Privacy privacy.Config
// Services contains config for services such as Google Analytics etc.
Services services.Config
}
type configLoader struct {
cfg config.Provider
ConfigSourceDescriptor
}
// Handle some legacy values.
func (l configLoader) applyConfigAliases() error {
aliases := []types.KeyValueStr{{Key: "taxonomies", Value: "indexes"}}
for _, alias := range aliases {
if l.cfg.IsSet(alias.Key) {
vv := l.cfg.Get(alias.Key)
l.cfg.Set(alias.Value, vv)
}
}
return nil
}
func (l configLoader) applyConfigDefaults() error {
defaultSettings := maps.Params{
"cleanDestinationDir": false,
"watch": false,
"resourceDir": "resources",
"publishDir": "public",
"themesDir": "themes",
"buildDrafts": false,
"buildFuture": false,
"buildExpired": false,
"environment": hugo.EnvironmentProduction,
"uglyURLs": false,
"verbose": false,
"ignoreCache": false,
"canonifyURLs": false,
"relativeURLs": false,
"removePathAccents": false,
"titleCaseStyle": "AP",
"taxonomies": map[string]string{"tag": "tags", "category": "categories"},
"permalinks": make(map[string]string),
"sitemap": config.Sitemap{Priority: -1, Filename: "sitemap.xml"},
"disableLiveReload": false,
"pluralizeListTitles": true,
"forceSyncStatic": false,
"footnoteAnchorPrefix": "",
"footnoteReturnLinkContents": "",
"newContentEditor": "",
"paginate": 10,
"paginatePath": "page",
"summaryLength": 70,
"rssLimit": -1,
"sectionPagesMenu": "",
"disablePathToLower": false,
"hasCJKLanguage": false,
"enableEmoji": false,
"pygmentsCodeFencesGuessSyntax": false,
"defaultContentLanguage": "en",
"defaultContentLanguageInSubdir": false,
"enableMissingTranslationPlaceholders": false,
"enableGitInfo": false,
"ignoreFiles": make([]string, 0),
"disableAliases": false,
"debug": false,
"disableFastRender": false,
"timeout": "30s",
"enableInlineShortcodes": false,
}
l.cfg.SetDefaults(defaultSettings)
return nil
}
func (l configLoader) applyOsEnvOverrides(environ []string) error {
if len(environ) == 0 {
return nil
}
const delim = "__env__delim"
// Extract all that start with the HUGO prefix.
// The delimiter is the following rune, usually "_".
const hugoEnvPrefix = "HUGO"
var hugoEnv []types.KeyValueStr
for _, v := range environ {
key, val := config.SplitEnvVar(v)
if strings.HasPrefix(key, hugoEnvPrefix) {
delimiterAndKey := strings.TrimPrefix(key, hugoEnvPrefix)
if len(delimiterAndKey) < 2 {
continue
}
// Allow delimiters to be case sensitive.
// It turns out there isn't that many allowed special
// chars in environment variables when used in Bash and similar,
// so variables on the form HUGOxPARAMSxFOO=bar is one option.
key := strings.ReplaceAll(delimiterAndKey[1:], delimiterAndKey[:1], delim)
key = strings.ToLower(key)
hugoEnv = append(hugoEnv, types.KeyValueStr{
Key: key,
Value: val,
})
}
}
for _, env := range hugoEnv {
existing, nestedKey, owner, err := maps.GetNestedParamFn(env.Key, delim, l.cfg.Get)
if err != nil {
return err
}
if existing != nil {
val, err := metadecoders.Default.UnmarshalStringTo(env.Value, existing)
if err != nil {
continue
}
if owner != nil {
owner[nestedKey] = val
} else {
l.cfg.Set(env.Key, val)
}
} else if nestedKey != "" {
owner[nestedKey] = env.Value
} else {
// The container does not exist yet.
l.cfg.Set(strings.ReplaceAll(env.Key, delim, "."), env.Value)
}
}
return nil
}
func (l configLoader) collectModules(modConfig modules.Config, v1 config.Provider, hookBeforeFinalize func(m *modules.ModulesConfig) error) (modules.Modules, []string, error) {
workingDir := l.WorkingDir
if workingDir == "" {
workingDir = v1.GetString("workingDir")
}
themesDir := paths.AbsPathify(l.WorkingDir, v1.GetString("themesDir"))
var ignoreVendor glob.Glob
if s := v1.GetString("ignoreVendorPaths"); s != "" {
ignoreVendor, _ = hglob.GetGlob(hglob.NormalizePath(s))
}
filecacheConfigs, err := filecache.DecodeConfig(l.Fs, v1)
if err != nil {
return nil, nil, err
}
v1.Set("filecacheConfigs", filecacheConfigs)
var configFilenames []string
hook := func(m *modules.ModulesConfig) error {
for _, tc := range m.ActiveModules {
if len(tc.ConfigFilenames()) > 0 {
if tc.Watch() {
configFilenames = append(configFilenames, tc.ConfigFilenames()...)
}
// Merge from theme config into v1 based on configured
// merge strategy.
v1.Merge("", tc.Cfg().Get(""))
}
}
if hookBeforeFinalize != nil {
return hookBeforeFinalize(m)
}
return nil
}
modulesClient := modules.NewClient(modules.ClientConfig{
Fs: l.Fs,
Logger: l.Logger,
HookBeforeFinalize: hook,
WorkingDir: workingDir,
ThemesDir: themesDir,
Environment: l.Environment,
CacheDir: filecacheConfigs.CacheDirModules(),
ModuleConfig: modConfig,
IgnoreVendor: ignoreVendor,
})
v1.Set("modulesClient", modulesClient)
moduleConfig, err := modulesClient.Collect()
// Avoid recreating these later.
v1.Set("allModules", moduleConfig.ActiveModules)
if moduleConfig.GoModulesFilename != "" {
// We want to watch this for changes and trigger rebuild on version
// changes etc.
configFilenames = append(configFilenames, moduleConfig.GoModulesFilename)
}
return moduleConfig.ActiveModules, configFilenames, err
}
func (l configLoader) loadConfig(configName string) (string, error) {
baseDir := l.configFileDir()
var baseFilename string
if filepath.IsAbs(configName) {
baseFilename = configName
} else {
baseFilename = filepath.Join(baseDir, configName)
}
var filename string
if cpaths.ExtNoDelimiter(configName) != "" {
exists, _ := helpers.Exists(baseFilename, l.Fs)
if exists {
filename = baseFilename
}
} else {
for _, ext := range config.ValidConfigFileExtensions {
filenameToCheck := baseFilename + "." + ext
exists, _ := helpers.Exists(filenameToCheck, l.Fs)
if exists {
filename = filenameToCheck
break
}
}
}
if filename == "" {
return "", ErrNoConfigFile
}
m, err := config.FromFileToMap(l.Fs, filename)
if err != nil {
return "", l.wrapFileError(err, filename)
}
// Set overwrites keys of the same name, recursively.
l.cfg.Set("", m)
return filename, nil
}
func (l configLoader) deleteMergeStrategies() {
l.cfg.WalkParams(func(params ...config.KeyParams) bool {
params[len(params)-1].Params.DeleteMergeStrategy()
return false
})
}
func (l configLoader) loadLanguageSettings(oldLangs langs.Languages) error {
_, err := langs.LoadLanguageSettings(l.cfg, oldLangs)
return err
}
func (l configLoader) loadModulesConfig() (modules.Config, error) {
modConfig, err := modules.DecodeConfig(l.cfg)
if err != nil {
return modules.Config{}, err
}
return modConfig, nil
}
func (configLoader) loadSiteConfig(cfg config.Provider) (scfg SiteConfig, err error) {
privacyConfig, err := privacy.DecodeConfig(cfg)
if err != nil {
return
}
servicesConfig, err := services.DecodeConfig(cfg)
if err != nil {
return
}
scfg.Privacy = privacyConfig
scfg.Services = servicesConfig
return
}
func (l configLoader) wrapFileError(err error, filename string) error {
err, _ = herrors.WithFileContextForFile(
err,
filename,
filename,
l.Fs,
herrors.SimpleLineMatcher)
return err
}