github.com/neohugo/neohugo@v0.123.8/config/allconfig/load.go (about)

     1  // Copyright 2024 The Hugo Authors. All rights reserved.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  // http://www.apache.org/licenses/LICENSE-2.0
     7  //
     8  // Unless required by applicable law or agreed to in writing, software
     9  // distributed under the License is distributed on an "AS IS" BASIS,
    10  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    11  // See the License for the specific language governing permissions and
    12  // limitations under the License.
    13  
    14  // Package allconfig contains the full configuration for Hugo.
    15  package allconfig
    16  
    17  import (
    18  	"errors"
    19  	"fmt"
    20  	"os"
    21  	"path/filepath"
    22  	"strings"
    23  
    24  	"github.com/gobwas/glob"
    25  	"github.com/neohugo/neohugo/common/herrors"
    26  	"github.com/neohugo/neohugo/common/hexec"
    27  	"github.com/neohugo/neohugo/common/loggers"
    28  	"github.com/neohugo/neohugo/common/maps"
    29  	"github.com/neohugo/neohugo/common/neohugo"
    30  	"github.com/neohugo/neohugo/common/paths"
    31  	"github.com/neohugo/neohugo/common/types"
    32  	"github.com/neohugo/neohugo/config"
    33  	"github.com/neohugo/neohugo/helpers"
    34  	hglob "github.com/neohugo/neohugo/hugofs/glob"
    35  	"github.com/neohugo/neohugo/modules"
    36  	"github.com/neohugo/neohugo/parser/metadecoders"
    37  	"github.com/spf13/afero"
    38  )
    39  
    40  //lint:ignore ST1005 end user message.
    41  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")
    42  
    43  func LoadConfig(d ConfigSourceDescriptor) (*Configs, error) {
    44  	if len(d.Environ) == 0 && !neohugo.IsRunningAsTest() {
    45  		d.Environ = os.Environ()
    46  	}
    47  
    48  	if d.Logger == nil {
    49  		d.Logger = loggers.NewDefault()
    50  	}
    51  
    52  	l := &configLoader{ConfigSourceDescriptor: d, cfg: config.New()}
    53  	// Make sure we always do this, even in error situations,
    54  	// as we have commands (e.g. "hugo mod init") that will
    55  	// use a partial configuration to do its job.
    56  	defer l.deleteMergeStrategies()
    57  	res, _, err := l.loadConfigMain(d)
    58  	if err != nil {
    59  		return nil, fmt.Errorf("failed to load config: %w", err)
    60  	}
    61  
    62  	configs, err := fromLoadConfigResult(d.Fs, d.Logger, res)
    63  	if err != nil {
    64  		return nil, fmt.Errorf("failed to create config from result: %w", err)
    65  	}
    66  
    67  	moduleConfig, modulesClient, err := l.loadModules(configs)
    68  	if err != nil {
    69  		return nil, fmt.Errorf("failed to load modules: %w", err)
    70  	}
    71  
    72  	if len(l.ModulesConfigFiles) > 0 {
    73  		// Config merged in from modules.
    74  		// Re-read the config.
    75  		configs, err = fromLoadConfigResult(d.Fs, d.Logger, res)
    76  		if err != nil {
    77  			return nil, fmt.Errorf("failed to create config from modules config: %w", err)
    78  		}
    79  		if err := configs.transientErr(); err != nil {
    80  			return nil, fmt.Errorf("failed to create config from modules config: %w", err)
    81  		}
    82  		configs.LoadingInfo.ConfigFiles = append(configs.LoadingInfo.ConfigFiles, l.ModulesConfigFiles...)
    83  	} else if err := configs.transientErr(); err != nil {
    84  		return nil, fmt.Errorf("failed to create config: %w", err)
    85  	}
    86  
    87  	configs.Modules = moduleConfig.AllModules
    88  	configs.ModulesClient = modulesClient
    89  
    90  	if err := configs.Init(); err != nil {
    91  		return nil, fmt.Errorf("failed to init config: %w", err)
    92  	}
    93  
    94  	loggers.InitGlobalLogger(d.Logger.Level(), configs.Base.PanicOnWarning)
    95  
    96  	return configs, nil
    97  }
    98  
    99  // ConfigSourceDescriptor describes where to find the config (e.g. config.toml etc.).
   100  type ConfigSourceDescriptor struct {
   101  	Fs     afero.Fs
   102  	Logger loggers.Logger
   103  
   104  	// Config received from the command line.
   105  	// These will override any config file settings.
   106  	Flags config.Provider
   107  
   108  	// Path to the config file to use, e.g. /my/project/config.toml
   109  	Filename string
   110  
   111  	// The (optional) directory for additional configuration files.
   112  	ConfigDir string
   113  
   114  	// production, development
   115  	Environment string
   116  
   117  	// Defaults to os.Environ if not set.
   118  	Environ []string
   119  }
   120  
   121  func (d ConfigSourceDescriptor) configFilenames() []string {
   122  	if d.Filename == "" {
   123  		return nil
   124  	}
   125  	return strings.Split(d.Filename, ",")
   126  }
   127  
   128  type configLoader struct {
   129  	cfg        config.Provider
   130  	BaseConfig config.BaseConfig
   131  	ConfigSourceDescriptor
   132  
   133  	// collected
   134  	ModulesConfig      modules.ModulesConfig
   135  	ModulesConfigFiles []string
   136  }
   137  
   138  // Handle some legacy values.
   139  func (l configLoader) applyConfigAliases() error {
   140  	aliases := []types.KeyValueStr{
   141  		{Key: "indexes", Value: "taxonomies"},
   142  		{Key: "logI18nWarnings", Value: "printI18nWarnings"},
   143  		{Key: "logPathWarnings", Value: "printPathWarnings"},
   144  		{Key: "ignoreErrors", Value: "ignoreLogs"},
   145  	}
   146  
   147  	for _, alias := range aliases {
   148  		if l.cfg.IsSet(alias.Key) {
   149  			vv := l.cfg.Get(alias.Key)
   150  			l.cfg.Set(alias.Value, vv)
   151  		}
   152  	}
   153  
   154  	return nil
   155  }
   156  
   157  func (l configLoader) applyDefaultConfig() error {
   158  	defaultSettings := maps.Params{
   159  		"baseURL":                              "",
   160  		"cleanDestinationDir":                  false,
   161  		"watch":                                false,
   162  		"contentDir":                           "content",
   163  		"resourceDir":                          "resources",
   164  		"publishDir":                           "public",
   165  		"publishDirOrig":                       "public",
   166  		"themesDir":                            "themes",
   167  		"assetDir":                             "assets",
   168  		"layoutDir":                            "layouts",
   169  		"i18nDir":                              "i18n",
   170  		"dataDir":                              "data",
   171  		"archetypeDir":                         "archetypes",
   172  		"configDir":                            "config",
   173  		"staticDir":                            "static",
   174  		"buildDrafts":                          false,
   175  		"buildFuture":                          false,
   176  		"buildExpired":                         false,
   177  		"params":                               maps.Params{},
   178  		"environment":                          neohugo.EnvironmentProduction,
   179  		"uglyURLs":                             false,
   180  		"verbose":                              false,
   181  		"ignoreCache":                          false,
   182  		"canonifyURLs":                         false,
   183  		"relativeURLs":                         false,
   184  		"removePathAccents":                    false,
   185  		"titleCaseStyle":                       "AP",
   186  		"taxonomies":                           maps.Params{"tag": "tags", "category": "categories"},
   187  		"permalinks":                           maps.Params{},
   188  		"sitemap":                              maps.Params{"priority": -1, "filename": "sitemap.xml"},
   189  		"menus":                                maps.Params{},
   190  		"disableLiveReload":                    false,
   191  		"pluralizeListTitles":                  true,
   192  		"capitalizeListTitles":                 true,
   193  		"forceSyncStatic":                      false,
   194  		"footnoteAnchorPrefix":                 "",
   195  		"footnoteReturnLinkContents":           "",
   196  		"newContentEditor":                     "",
   197  		"paginate":                             10,
   198  		"paginatePath":                         "page",
   199  		"summaryLength":                        70,
   200  		"rssLimit":                             -1,
   201  		"sectionPagesMenu":                     "",
   202  		"disablePathToLower":                   false,
   203  		"hasCJKLanguage":                       false,
   204  		"enableEmoji":                          false,
   205  		"defaultContentLanguage":               "en",
   206  		"defaultContentLanguageInSubdir":       false,
   207  		"enableMissingTranslationPlaceholders": false,
   208  		"enableGitInfo":                        false,
   209  		"ignoreFiles":                          make([]string, 0),
   210  		"disableAliases":                       false,
   211  		"debug":                                false,
   212  		"disableFastRender":                    false,
   213  		"timeout":                              "30s",
   214  		"timeZone":                             "",
   215  		"enableInlineShortcodes":               false,
   216  	}
   217  
   218  	l.cfg.SetDefaults(defaultSettings)
   219  
   220  	return nil
   221  }
   222  
   223  func (l configLoader) normalizeCfg(cfg config.Provider) error {
   224  	if b, ok := cfg.Get("minifyOutput").(bool); ok && b {
   225  		cfg.Set("minify.minifyOutput", true)
   226  	} else if b, ok := cfg.Get("minify").(bool); ok && b {
   227  		cfg.Set("minify", maps.Params{"minifyOutput": true})
   228  	}
   229  
   230  	return nil
   231  }
   232  
   233  func (l configLoader) cleanExternalConfig(cfg config.Provider) error {
   234  	if cfg.IsSet("internal") {
   235  		cfg.Set("internal", nil)
   236  	}
   237  	return nil
   238  }
   239  
   240  func (l configLoader) applyFlagsOverrides(cfg config.Provider) error {
   241  	for _, k := range cfg.Keys() {
   242  		l.cfg.Set(k, cfg.Get(k))
   243  	}
   244  	return nil
   245  }
   246  
   247  func (l configLoader) applyOsEnvOverrides(environ []string) error {
   248  	if len(environ) == 0 {
   249  		return nil
   250  	}
   251  
   252  	const delim = "__env__delim"
   253  
   254  	// Extract all that start with the HUGO prefix.
   255  	// The delimiter is the following rune, usually "_".
   256  	const hugoEnvPrefix = "HUGO"
   257  	var hugoEnv []types.KeyValueStr
   258  	for _, v := range environ {
   259  		key, val := config.SplitEnvVar(v)
   260  		if strings.HasPrefix(key, hugoEnvPrefix) {
   261  			delimiterAndKey := strings.TrimPrefix(key, hugoEnvPrefix)
   262  			if len(delimiterAndKey) < 2 {
   263  				continue
   264  			}
   265  			// Allow delimiters to be case sensitive.
   266  			// It turns out there isn't that many allowed special
   267  			// chars in environment variables when used in Bash and similar,
   268  			// so variables on the form HUGOxPARAMSxFOO=bar is one option.
   269  			key := strings.ReplaceAll(delimiterAndKey[1:], delimiterAndKey[:1], delim)
   270  			key = strings.ToLower(key)
   271  			hugoEnv = append(hugoEnv, types.KeyValueStr{
   272  				Key:   key,
   273  				Value: val,
   274  			})
   275  
   276  		}
   277  	}
   278  
   279  	for _, env := range hugoEnv {
   280  		existing, nestedKey, owner, err := maps.GetNestedParamFn(env.Key, delim, l.cfg.Get)
   281  		if err != nil {
   282  			return err
   283  		}
   284  
   285  		if existing != nil {
   286  			val, err := metadecoders.Default.UnmarshalStringTo(env.Value, existing)
   287  			if err != nil {
   288  				continue
   289  			}
   290  
   291  			if owner != nil {
   292  				owner[nestedKey] = val
   293  			} else {
   294  				l.cfg.Set(env.Key, val)
   295  			}
   296  		} else {
   297  			if nestedKey != "" {
   298  				owner[nestedKey] = env.Value
   299  			} else {
   300  				var val any
   301  				key := strings.ReplaceAll(env.Key, delim, ".")
   302  				_, ok := allDecoderSetups[key]
   303  				if ok {
   304  					// A map.
   305  					if v, err := metadecoders.Default.UnmarshalStringTo(env.Value, map[string]interface{}{}); err == nil {
   306  						val = v
   307  					}
   308  				}
   309  				if val == nil {
   310  					// A string.
   311  					val = l.envStringToVal(key, env.Value)
   312  				}
   313  				l.cfg.Set(key, val)
   314  			}
   315  		}
   316  	}
   317  
   318  	return nil
   319  }
   320  
   321  func (l *configLoader) envStringToVal(k, v string) any {
   322  	switch k {
   323  	case "disablekinds", "disablelanguages":
   324  		if strings.Contains(v, ",") {
   325  			return strings.Split(v, ",")
   326  		} else {
   327  			return strings.Fields(v)
   328  		}
   329  	default:
   330  		return v
   331  	}
   332  }
   333  
   334  func (l *configLoader) loadConfigMain(d ConfigSourceDescriptor) (config.LoadConfigResult, modules.ModulesConfig, error) {
   335  	var res config.LoadConfigResult
   336  
   337  	if d.Flags != nil {
   338  		if err := l.normalizeCfg(d.Flags); err != nil {
   339  			return res, l.ModulesConfig, err
   340  		}
   341  	}
   342  
   343  	if d.Fs == nil {
   344  		return res, l.ModulesConfig, errors.New("no filesystem provided")
   345  	}
   346  
   347  	if d.Flags != nil {
   348  		if err := l.applyFlagsOverrides(d.Flags); err != nil {
   349  			return res, l.ModulesConfig, err
   350  		}
   351  		workingDir := filepath.Clean(l.cfg.GetString("workingDir"))
   352  
   353  		l.BaseConfig = config.BaseConfig{
   354  			WorkingDir: workingDir,
   355  			ThemesDir:  paths.AbsPathify(workingDir, l.cfg.GetString("themesDir")),
   356  		}
   357  
   358  	}
   359  
   360  	names := d.configFilenames()
   361  
   362  	if names != nil {
   363  		for _, name := range names {
   364  			var filename string
   365  			filename, err := l.loadConfig(name)
   366  			if err == nil {
   367  				res.ConfigFiles = append(res.ConfigFiles, filename)
   368  			} else if err != ErrNoConfigFile {
   369  				return res, l.ModulesConfig, l.wrapFileError(err, filename)
   370  			}
   371  		}
   372  	} else {
   373  		for _, name := range config.DefaultConfigNames {
   374  			var filename string
   375  			filename, err := l.loadConfig(name)
   376  			if err == nil {
   377  				res.ConfigFiles = append(res.ConfigFiles, filename)
   378  				break
   379  			} else if err != ErrNoConfigFile {
   380  				return res, l.ModulesConfig, l.wrapFileError(err, filename)
   381  			}
   382  		}
   383  	}
   384  
   385  	if d.ConfigDir != "" {
   386  		absConfigDir := paths.AbsPathify(l.BaseConfig.WorkingDir, d.ConfigDir)
   387  		dcfg, dirnames, err := config.LoadConfigFromDir(l.Fs, absConfigDir, l.Environment)
   388  		if err == nil {
   389  			if len(dirnames) > 0 {
   390  				if err := l.normalizeCfg(dcfg); err != nil {
   391  					return res, l.ModulesConfig, err
   392  				}
   393  				if err := l.cleanExternalConfig(dcfg); err != nil {
   394  					return res, l.ModulesConfig, err
   395  				}
   396  				l.cfg.Set("", dcfg.Get(""))
   397  				res.ConfigFiles = append(res.ConfigFiles, dirnames...)
   398  			}
   399  		} else if err != ErrNoConfigFile {
   400  			if len(dirnames) > 0 {
   401  				return res, l.ModulesConfig, l.wrapFileError(err, dirnames[0])
   402  			}
   403  			return res, l.ModulesConfig, err
   404  		}
   405  	}
   406  
   407  	res.Cfg = l.cfg
   408  
   409  	if err := l.applyDefaultConfig(); err != nil {
   410  		return res, l.ModulesConfig, err
   411  	}
   412  
   413  	// Some settings are used before we're done collecting all settings,
   414  	// so apply OS environment both before and after.
   415  	if err := l.applyOsEnvOverrides(d.Environ); err != nil {
   416  		return res, l.ModulesConfig, err
   417  	}
   418  
   419  	workingDir := filepath.Clean(l.cfg.GetString("workingDir"))
   420  
   421  	l.BaseConfig = config.BaseConfig{
   422  		WorkingDir: workingDir,
   423  		CacheDir:   l.cfg.GetString("cacheDir"),
   424  		ThemesDir:  paths.AbsPathify(workingDir, l.cfg.GetString("themesDir")),
   425  	}
   426  
   427  	var err error
   428  	l.BaseConfig.CacheDir, err = helpers.GetCacheDir(l.Fs, l.BaseConfig.CacheDir)
   429  	if err != nil {
   430  		return res, l.ModulesConfig, err
   431  	}
   432  
   433  	res.BaseConfig = l.BaseConfig
   434  
   435  	l.cfg.SetDefaultMergeStrategy()
   436  
   437  	res.ConfigFiles = append(res.ConfigFiles, l.ModulesConfigFiles...)
   438  
   439  	if d.Flags != nil {
   440  		if err := l.applyFlagsOverrides(d.Flags); err != nil {
   441  			return res, l.ModulesConfig, err
   442  		}
   443  	}
   444  
   445  	if err := l.applyOsEnvOverrides(d.Environ); err != nil {
   446  		return res, l.ModulesConfig, err
   447  	}
   448  
   449  	if err = l.applyConfigAliases(); err != nil {
   450  		return res, l.ModulesConfig, err
   451  	}
   452  
   453  	return res, l.ModulesConfig, err
   454  }
   455  
   456  func (l *configLoader) loadModules(configs *Configs) (modules.ModulesConfig, *modules.Client, error) {
   457  	bcfg := configs.LoadingInfo.BaseConfig
   458  	conf := configs.Base
   459  	workingDir := bcfg.WorkingDir
   460  	themesDir := bcfg.ThemesDir
   461  
   462  	cfg := configs.LoadingInfo.Cfg
   463  
   464  	var ignoreVendor glob.Glob
   465  	if s := conf.IgnoreVendorPaths; s != "" {
   466  		ignoreVendor, _ = hglob.GetGlob(hglob.NormalizePath(s))
   467  	}
   468  
   469  	ex := hexec.New(conf.Security)
   470  
   471  	hook := func(m *modules.ModulesConfig) error {
   472  		for _, tc := range m.AllModules {
   473  			if len(tc.ConfigFilenames()) > 0 {
   474  				if tc.Watch() {
   475  					l.ModulesConfigFiles = append(l.ModulesConfigFiles, tc.ConfigFilenames()...)
   476  				}
   477  
   478  				// Merge in the theme config using the configured
   479  				// merge strategy.
   480  				cfg.Merge("", tc.Cfg().Get(""))
   481  
   482  			}
   483  		}
   484  
   485  		return nil
   486  	}
   487  
   488  	modulesClient := modules.NewClient(modules.ClientConfig{
   489  		Fs:                 l.Fs,
   490  		Logger:             l.Logger,
   491  		Exec:               ex,
   492  		HookBeforeFinalize: hook,
   493  		WorkingDir:         workingDir,
   494  		ThemesDir:          themesDir,
   495  		Environment:        l.Environment,
   496  		CacheDir:           conf.Caches.CacheDirModules(),
   497  		ModuleConfig:       conf.Module,
   498  		IgnoreVendor:       ignoreVendor,
   499  	})
   500  
   501  	moduleConfig, err := modulesClient.Collect()
   502  
   503  	// We want to watch these for changes and trigger rebuild on version
   504  	// changes etc.
   505  	if moduleConfig.GoModulesFilename != "" {
   506  		l.ModulesConfigFiles = append(l.ModulesConfigFiles, moduleConfig.GoModulesFilename)
   507  	}
   508  
   509  	if moduleConfig.GoWorkspaceFilename != "" {
   510  		l.ModulesConfigFiles = append(l.ModulesConfigFiles, moduleConfig.GoWorkspaceFilename)
   511  	}
   512  
   513  	return moduleConfig, modulesClient, err
   514  }
   515  
   516  func (l configLoader) loadConfig(configName string) (string, error) {
   517  	baseDir := l.BaseConfig.WorkingDir
   518  	var baseFilename string
   519  	if filepath.IsAbs(configName) {
   520  		baseFilename = configName
   521  	} else {
   522  		baseFilename = filepath.Join(baseDir, configName)
   523  	}
   524  
   525  	var filename string
   526  	if paths.ExtNoDelimiter(configName) != "" {
   527  		exists, _ := helpers.Exists(baseFilename, l.Fs)
   528  		if exists {
   529  			filename = baseFilename
   530  		}
   531  	} else {
   532  		for _, ext := range config.ValidConfigFileExtensions {
   533  			filenameToCheck := baseFilename + "." + ext
   534  			exists, _ := helpers.Exists(filenameToCheck, l.Fs)
   535  			if exists {
   536  				filename = filenameToCheck
   537  				break
   538  			}
   539  		}
   540  	}
   541  
   542  	if filename == "" {
   543  		return "", ErrNoConfigFile
   544  	}
   545  
   546  	m, err := config.FromFileToMap(l.Fs, filename)
   547  	if err != nil {
   548  		return filename, err
   549  	}
   550  
   551  	// Set overwrites keys of the same name, recursively.
   552  	l.cfg.Set("", m)
   553  
   554  	if err := l.normalizeCfg(l.cfg); err != nil {
   555  		return filename, err
   556  	}
   557  
   558  	if err := l.cleanExternalConfig(l.cfg); err != nil {
   559  		return filename, err
   560  	}
   561  
   562  	return filename, nil
   563  }
   564  
   565  func (l configLoader) deleteMergeStrategies() {
   566  	l.cfg.WalkParams(func(params ...maps.KeyParams) bool {
   567  		params[len(params)-1].Params.DeleteMergeStrategy()
   568  		return false
   569  	})
   570  }
   571  
   572  func (l configLoader) wrapFileError(err error, filename string) error {
   573  	fe := herrors.UnwrapFileError(err)
   574  	if fe != nil {
   575  		pos := fe.Position()
   576  		pos.Filename = filename
   577  		fe.UpdatePosition(pos) // nolint
   578  		return err
   579  	}
   580  	return herrors.NewFileErrorFromFile(err, filename, l.Fs, nil)
   581  }