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 }