// Copyright 2024 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 allconfig contains the full configuration for Hugo. package allconfig import ( "errors" "fmt" "os" "path/filepath" "strings" "github.com/gobwas/glob" "github.com/gohugoio/hugo/common/herrors" "github.com/gohugoio/hugo/common/hexec" "github.com/gohugoio/hugo/common/hugo" "github.com/gohugoio/hugo/common/loggers" "github.com/gohugoio/hugo/common/maps" "github.com/gohugoio/hugo/common/paths" "github.com/gohugoio/hugo/common/types" "github.com/gohugoio/hugo/config" "github.com/gohugoio/hugo/helpers" hglob "github.com/gohugoio/hugo/hugofs/glob" "github.com/gohugoio/hugo/modules" "github.com/gohugoio/hugo/parser/metadecoders" "github.com/spf13/afero" ) //lint:ignore ST1005 end user message. 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") func LoadConfig(d ConfigSourceDescriptor) (*Configs, error) { if len(d.Environ) == 0 && !hugo.IsRunningAsTest() { d.Environ = os.Environ() } if d.Logger == nil { d.Logger = loggers.NewDefault() } 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() res, _, err := l.loadConfigMain(d) if err != nil { return nil, fmt.Errorf("failed to load config: %w", err) } configs, err := fromLoadConfigResult(d.Fs, d.Logger, res) if err != nil { return nil, fmt.Errorf("failed to create config from result: %w", err) } moduleConfig, modulesClient, err := l.loadModules(configs) if err != nil { return nil, fmt.Errorf("failed to load modules: %w", err) } if len(l.ModulesConfigFiles) > 0 { // Config merged in from modules. // Re-read the config. configs, err = fromLoadConfigResult(d.Fs, d.Logger, res) if err != nil { return nil, fmt.Errorf("failed to create config from modules config: %w", err) } if err := configs.transientErr(); err != nil { return nil, fmt.Errorf("failed to create config from modules config: %w", err) } configs.LoadingInfo.ConfigFiles = append(configs.LoadingInfo.ConfigFiles, l.ModulesConfigFiles...) } else if err := configs.transientErr(); err != nil { return nil, fmt.Errorf("failed to create config: %w", err) } configs.Modules = moduleConfig.AllModules configs.ModulesClient = modulesClient if err := configs.Init(); err != nil { return nil, fmt.Errorf("failed to init config: %w", err) } loggers.InitGlobalLogger(d.Logger.Level(), configs.Base.PanicOnWarning) return configs, nil } // ConfigSourceDescriptor describes where to find the config (e.g. config.toml etc.). type ConfigSourceDescriptor struct { Fs afero.Fs Logger loggers.Logger // Config received from the command line. // These will override any config file settings. Flags config.Provider // Path to the config file to use, e.g. /my/project/config.toml Filename string // The (optional) directory for additional configuration files. ConfigDir string // production, development Environment string // Defaults to os.Environ if not set. Environ []string } func (d ConfigSourceDescriptor) configFilenames() []string { if d.Filename == "" { return nil } return strings.Split(d.Filename, ",") } type configLoader struct { cfg config.Provider BaseConfig config.BaseConfig ConfigSourceDescriptor // collected ModulesConfig modules.ModulesConfig ModulesConfigFiles []string } // Handle some legacy values. func (l configLoader) applyConfigAliases() error { aliases := []types.KeyValueStr{ {Key: "indexes", Value: "taxonomies"}, {Key: "logI18nWarnings", Value: "printI18nWarnings"}, {Key: "logPathWarnings", Value: "printPathWarnings"}, {Key: "ignoreErrors", Value: "ignoreLogs"}, } 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) applyDefaultConfig() error { defaultSettings := maps.Params{ "baseURL": "", "cleanDestinationDir": false, "watch": false, "contentDir": "content", "resourceDir": "resources", "publishDir": "public", "publishDirOrig": "public", "themesDir": "themes", "assetDir": "assets", "layoutDir": "layouts", "i18nDir": "i18n", "dataDir": "data", "archetypeDir": "archetypes", "configDir": "config", "staticDir": "static", "buildDrafts": false, "buildFuture": false, "buildExpired": false, "params": maps.Params{}, "environment": hugo.EnvironmentProduction, "uglyURLs": false, "verbose": false, "ignoreCache": false, "canonifyURLs": false, "relativeURLs": false, "removePathAccents": false, "titleCaseStyle": "AP", "taxonomies": maps.Params{"tag": "tags", "category": "categories"}, "permalinks": maps.Params{}, "sitemap": maps.Params{"priority": -1, "filename": "sitemap.xml"}, "menus": maps.Params{}, "disableLiveReload": false, "pluralizeListTitles": true, "capitalizeListTitles": true, "forceSyncStatic": false, "footnoteAnchorPrefix": "", "footnoteReturnLinkContents": "", "newContentEditor": "", "paginate": 0, // Moved into the paginator struct in Hugo v0.128.0. "paginatePath": "", // Moved into the paginator struct in Hugo v0.128.0. "summaryLength": 70, "rssLimit": -1, "sectionPagesMenu": "", "disablePathToLower": false, "hasCJKLanguage": false, "enableEmoji": false, "defaultContentLanguage": "en", "defaultContentLanguageInSubdir": false, "enableMissingTranslationPlaceholders": false, "enableGitInfo": false, "ignoreFiles": make([]string, 0), "disableAliases": false, "debug": false, "disableFastRender": false, "timeout": "30s", "timeZone": "", "enableInlineShortcodes": false, } l.cfg.SetDefaults(defaultSettings) return nil } func (l configLoader) normalizeCfg(cfg config.Provider) error { if b, ok := cfg.Get("minifyOutput").(bool); ok && b { cfg.Set("minify.minifyOutput", true) } else if b, ok := cfg.Get("minify").(bool); ok && b { cfg.Set("minify", maps.Params{"minifyOutput": true}) } return nil } func (l configLoader) cleanExternalConfig(cfg config.Provider) error { if cfg.IsSet("internal") { cfg.Set("internal", nil) } return nil } func (l configLoader) applyFlagsOverrides(cfg config.Provider) error { for _, k := range cfg.Keys() { l.cfg.Set(k, cfg.Get(k)) } 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 { var val any key := strings.ReplaceAll(env.Key, delim, ".") _, ok := allDecoderSetups[key] if ok { // A map. if v, err := metadecoders.Default.UnmarshalStringTo(env.Value, map[string]interface{}{}); err == nil { val = v } } if val == nil { // A string. val = l.envStringToVal(key, env.Value) } l.cfg.Set(key, val) } } } return nil } func (l *configLoader) envStringToVal(k, v string) any { switch k { case "disablekinds", "disablelanguages": if strings.Contains(v, ",") { return strings.Split(v, ",") } else { return strings.Fields(v) } default: return v } } func (l *configLoader) loadConfigMain(d ConfigSourceDescriptor) (config.LoadConfigResult, modules.ModulesConfig, error) { var res config.LoadConfigResult if d.Flags != nil { if err := l.normalizeCfg(d.Flags); err != nil { return res, l.ModulesConfig, err } } if d.Fs == nil { return res, l.ModulesConfig, errors.New("no filesystem provided") } if d.Flags != nil { if err := l.applyFlagsOverrides(d.Flags); err != nil { return res, l.ModulesConfig, err } workingDir := filepath.Clean(l.cfg.GetString("workingDir")) l.BaseConfig = config.BaseConfig{ WorkingDir: workingDir, ThemesDir: paths.AbsPathify(workingDir, l.cfg.GetString("themesDir")), } } names := d.configFilenames() if names != nil { for _, name := range names { var filename string filename, err := l.loadConfig(name) if err == nil { res.ConfigFiles = append(res.ConfigFiles, filename) } else if err != ErrNoConfigFile { return res, l.ModulesConfig, l.wrapFileError(err, filename) } } } else { for _, name := range config.DefaultConfigNames { var filename string filename, err := l.loadConfig(name) if err == nil { res.ConfigFiles = append(res.ConfigFiles, filename) break } else if err != ErrNoConfigFile { return res, l.ModulesConfig, l.wrapFileError(err, filename) } } } if d.ConfigDir != "" { absConfigDir := paths.AbsPathify(l.BaseConfig.WorkingDir, d.ConfigDir) dcfg, dirnames, err := config.LoadConfigFromDir(l.Fs, absConfigDir, l.Environment) if err == nil { if len(dirnames) > 0 { if err := l.normalizeCfg(dcfg); err != nil { return res, l.ModulesConfig, err } if err := l.cleanExternalConfig(dcfg); err != nil { return res, l.ModulesConfig, err } l.cfg.Set("", dcfg.Get("")) res.ConfigFiles = append(res.ConfigFiles, dirnames...) } } else if err != ErrNoConfigFile { if len(dirnames) > 0 { return res, l.ModulesConfig, l.wrapFileError(err, dirnames[0]) } return res, l.ModulesConfig, err } } res.Cfg = l.cfg if err := l.applyDefaultConfig(); err != nil { return res, l.ModulesConfig, err } // 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 res, l.ModulesConfig, err } workingDir := filepath.Clean(l.cfg.GetString("workingDir")) l.BaseConfig = config.BaseConfig{ WorkingDir: workingDir, CacheDir: l.cfg.GetString("cacheDir"), ThemesDir: paths.AbsPathify(workingDir, l.cfg.GetString("themesDir")), } var err error l.BaseConfig.CacheDir, err = helpers.GetCacheDir(l.Fs, l.BaseConfig.CacheDir) if err != nil { return res, l.ModulesConfig, err } res.BaseConfig = l.BaseConfig l.cfg.SetDefaultMergeStrategy() res.ConfigFiles = append(res.ConfigFiles, l.ModulesConfigFiles...) if d.Flags != nil { if err := l.applyFlagsOverrides(d.Flags); err != nil { return res, l.ModulesConfig, err } } if err := l.applyOsEnvOverrides(d.Environ); err != nil { return res, l.ModulesConfig, err } if err = l.applyConfigAliases(); err != nil { return res, l.ModulesConfig, err } return res, l.ModulesConfig, err } func (l *configLoader) loadModules(configs *Configs) (modules.ModulesConfig, *modules.Client, error) { bcfg := configs.LoadingInfo.BaseConfig conf := configs.Base workingDir := bcfg.WorkingDir themesDir := bcfg.ThemesDir publishDir := bcfg.PublishDir cfg := configs.LoadingInfo.Cfg var ignoreVendor glob.Glob if s := conf.IgnoreVendorPaths; s != "" { ignoreVendor, _ = hglob.GetGlob(hglob.NormalizePath(s)) } ex := hexec.New(conf.Security, workingDir) hook := func(m *modules.ModulesConfig) error { for _, tc := range m.AllModules { if len(tc.ConfigFilenames()) > 0 { if tc.Watch() { l.ModulesConfigFiles = append(l.ModulesConfigFiles, tc.ConfigFilenames()...) } // Merge in the theme config using the configured // merge strategy. cfg.Merge("", tc.Cfg().Get("")) } } return nil } modulesClient := modules.NewClient(modules.ClientConfig{ Fs: l.Fs, Logger: l.Logger, Exec: ex, HookBeforeFinalize: hook, WorkingDir: workingDir, ThemesDir: themesDir, PublishDir: publishDir, Environment: l.Environment, CacheDir: conf.Caches.CacheDirModules(), ModuleConfig: conf.Module, IgnoreVendor: ignoreVendor, }) moduleConfig, err := modulesClient.Collect() // We want to watch these for changes and trigger rebuild on version // changes etc. if moduleConfig.GoModulesFilename != "" { l.ModulesConfigFiles = append(l.ModulesConfigFiles, moduleConfig.GoModulesFilename) } if moduleConfig.GoWorkspaceFilename != "" { l.ModulesConfigFiles = append(l.ModulesConfigFiles, moduleConfig.GoWorkspaceFilename) } return moduleConfig, modulesClient, err } func (l configLoader) loadConfig(configName string) (string, error) { baseDir := l.BaseConfig.WorkingDir var baseFilename string if filepath.IsAbs(configName) { baseFilename = configName } else { baseFilename = filepath.Join(baseDir, configName) } var filename string if paths.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 filename, err } // Set overwrites keys of the same name, recursively. l.cfg.Set("", m) if err := l.normalizeCfg(l.cfg); err != nil { return filename, err } if err := l.cleanExternalConfig(l.cfg); err != nil { return filename, err } return filename, nil } func (l configLoader) deleteMergeStrategies() { l.cfg.WalkParams(func(params ...maps.KeyParams) bool { params[len(params)-1].Params.DeleteMergeStrategy() return false }) } func (l configLoader) wrapFileError(err error, filename string) error { fe := herrors.UnwrapFileError(err) if fe != nil { pos := fe.Position() pos.Filename = filename fe.UpdatePosition(pos) return err } return herrors.NewFileErrorFromFile(err, filename, l.Fs, nil) }