github.com/gohugoio/hugo@v0.88.1/hugolib/config.go (about) 1 // Copyright 2019 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 hugolib 15 16 import ( 17 "os" 18 "path/filepath" 19 "strings" 20 21 "github.com/gohugoio/hugo/common/types" 22 23 "github.com/gohugoio/hugo/common/maps" 24 cpaths "github.com/gohugoio/hugo/common/paths" 25 26 "github.com/gobwas/glob" 27 hglob "github.com/gohugoio/hugo/hugofs/glob" 28 29 "github.com/gohugoio/hugo/common/loggers" 30 31 "github.com/gohugoio/hugo/cache/filecache" 32 33 "github.com/gohugoio/hugo/parser/metadecoders" 34 35 "github.com/gohugoio/hugo/common/herrors" 36 "github.com/gohugoio/hugo/common/hugo" 37 "github.com/gohugoio/hugo/hugolib/paths" 38 "github.com/gohugoio/hugo/langs" 39 "github.com/gohugoio/hugo/modules" 40 "github.com/pkg/errors" 41 42 "github.com/gohugoio/hugo/config" 43 "github.com/gohugoio/hugo/config/privacy" 44 "github.com/gohugoio/hugo/config/services" 45 "github.com/gohugoio/hugo/helpers" 46 "github.com/spf13/afero" 47 ) 48 49 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") 50 51 // LoadConfig loads Hugo configuration into a new Viper and then adds 52 // a set of defaults. 53 func LoadConfig(d ConfigSourceDescriptor, doWithConfig ...func(cfg config.Provider) error) (config.Provider, []string, error) { 54 55 if d.Environment == "" { 56 d.Environment = hugo.EnvironmentProduction 57 } 58 59 if len(d.Environ) == 0 && !hugo.IsRunningAsTest() { 60 d.Environ = os.Environ() 61 } 62 63 var configFiles []string 64 65 l := configLoader{ConfigSourceDescriptor: d, cfg: config.New()} 66 // Make sure we always do this, even in error situations, 67 // as we have commands (e.g. "hugo mod init") that will 68 // use a partial configuration to do its job. 69 defer l.deleteMergeStrategies() 70 71 for _, name := range d.configFilenames() { 72 var filename string 73 filename, err := l.loadConfig(name) 74 if err == nil { 75 configFiles = append(configFiles, filename) 76 } else if err != ErrNoConfigFile { 77 return nil, nil, err 78 } 79 } 80 81 if d.AbsConfigDir != "" { 82 dcfg, dirnames, err := config.LoadConfigFromDir(l.Fs, d.AbsConfigDir, l.Environment) 83 if err == nil { 84 if len(dirnames) > 0 { 85 l.cfg.Set("", dcfg.Get("")) 86 configFiles = append(configFiles, dirnames...) 87 } 88 } else if err != ErrNoConfigFile { 89 if len(dirnames) > 0 { 90 return nil, nil, l.wrapFileError(err, dirnames[0]) 91 } 92 return nil, nil, err 93 } 94 } 95 96 if err := l.applyConfigDefaults(); err != nil { 97 return l.cfg, configFiles, err 98 } 99 100 l.cfg.SetDefaultMergeStrategy() 101 102 // We create languages based on the settings, so we need to make sure that 103 // all configuration is loaded/set before doing that. 104 for _, d := range doWithConfig { 105 if err := d(l.cfg); err != nil { 106 return l.cfg, configFiles, err 107 } 108 } 109 110 // Config deprecations. 111 // We made this a Glob pattern in Hugo 0.75, we don't need both. 112 if l.cfg.GetBool("ignoreVendor") { 113 helpers.Deprecated("--ignoreVendor", "--ignoreVendorPaths **", true) 114 l.cfg.Set("ignoreVendorPaths", "**") 115 } 116 117 if l.cfg.GetString("markup.defaultMarkdownHandler") == "blackfriday" { 118 helpers.Deprecated("markup.defaultMarkdownHandler=blackfriday", "See https://gohugo.io//content-management/formats/#list-of-content-formats", false) 119 120 } 121 122 // Some settings are used before we're done collecting all settings, 123 // so apply OS environment both before and after. 124 if err := l.applyOsEnvOverrides(d.Environ); err != nil { 125 return l.cfg, configFiles, err 126 } 127 128 modulesConfig, err := l.loadModulesConfig() 129 if err != nil { 130 return l.cfg, configFiles, err 131 } 132 133 // Need to run these after the modules are loaded, but before 134 // they are finalized. 135 collectHook := func(m *modules.ModulesConfig) error { 136 // We don't need the merge strategy configuration anymore, 137 // remove it so it doesn't accidentaly show up in other settings. 138 l.deleteMergeStrategies() 139 140 if err := l.loadLanguageSettings(nil); err != nil { 141 return err 142 } 143 144 mods := m.ActiveModules 145 146 // Apply default project mounts. 147 if err := modules.ApplyProjectConfigDefaults(l.cfg, mods[0]); err != nil { 148 return err 149 } 150 151 return nil 152 } 153 154 _, modulesConfigFiles, modulesCollectErr := l.collectModules(modulesConfig, l.cfg, collectHook) 155 if err != nil { 156 return l.cfg, configFiles, err 157 } 158 159 configFiles = append(configFiles, modulesConfigFiles...) 160 161 if err := l.applyOsEnvOverrides(d.Environ); err != nil { 162 return l.cfg, configFiles, err 163 } 164 165 if err = l.applyConfigAliases(); err != nil { 166 return l.cfg, configFiles, err 167 } 168 169 if err == nil { 170 err = modulesCollectErr 171 } 172 173 return l.cfg, configFiles, err 174 } 175 176 // LoadConfigDefault is a convenience method to load the default "config.toml" config. 177 func LoadConfigDefault(fs afero.Fs) (config.Provider, error) { 178 v, _, err := LoadConfig(ConfigSourceDescriptor{Fs: fs, Filename: "config.toml"}) 179 return v, err 180 } 181 182 // ConfigSourceDescriptor describes where to find the config (e.g. config.toml etc.). 183 type ConfigSourceDescriptor struct { 184 Fs afero.Fs 185 Logger loggers.Logger 186 187 // Path to the config file to use, e.g. /my/project/config.toml 188 Filename string 189 190 // The path to the directory to look for configuration. Is used if Filename is not 191 // set or if it is set to a relative filename. 192 Path string 193 194 // The project's working dir. Is used to look for additional theme config. 195 WorkingDir string 196 197 // The (optional) directory for additional configuration files. 198 AbsConfigDir string 199 200 // production, development 201 Environment string 202 203 // Defaults to os.Environ if not set. 204 Environ []string 205 } 206 207 func (d ConfigSourceDescriptor) configFileDir() string { 208 if d.Path != "" { 209 return d.Path 210 } 211 return d.WorkingDir 212 } 213 214 func (d ConfigSourceDescriptor) configFilenames() []string { 215 if d.Filename == "" { 216 return []string{"config"} 217 } 218 return strings.Split(d.Filename, ",") 219 } 220 221 // SiteConfig represents the config in .Site.Config. 222 type SiteConfig struct { 223 // This contains all privacy related settings that can be used to 224 // make the YouTube template etc. GDPR compliant. 225 Privacy privacy.Config 226 227 // Services contains config for services such as Google Analytics etc. 228 Services services.Config 229 } 230 231 type configLoader struct { 232 cfg config.Provider 233 ConfigSourceDescriptor 234 } 235 236 // Handle some legacy values. 237 func (l configLoader) applyConfigAliases() error { 238 aliases := []types.KeyValueStr{{Key: "taxonomies", Value: "indexes"}} 239 240 for _, alias := range aliases { 241 if l.cfg.IsSet(alias.Key) { 242 vv := l.cfg.Get(alias.Key) 243 l.cfg.Set(alias.Value, vv) 244 } 245 } 246 247 return nil 248 } 249 250 func (l configLoader) applyConfigDefaults() error { 251 defaultSettings := maps.Params{ 252 "cleanDestinationDir": false, 253 "watch": false, 254 "resourceDir": "resources", 255 "publishDir": "public", 256 "themesDir": "themes", 257 "buildDrafts": false, 258 "buildFuture": false, 259 "buildExpired": false, 260 "environment": hugo.EnvironmentProduction, 261 "uglyURLs": false, 262 "verbose": false, 263 "ignoreCache": false, 264 "canonifyURLs": false, 265 "relativeURLs": false, 266 "removePathAccents": false, 267 "titleCaseStyle": "AP", 268 "taxonomies": maps.Params{"tag": "tags", "category": "categories"}, 269 "permalinks": maps.Params{}, 270 "sitemap": maps.Params{"priority": -1, "filename": "sitemap.xml"}, 271 "disableLiveReload": false, 272 "pluralizeListTitles": true, 273 "forceSyncStatic": false, 274 "footnoteAnchorPrefix": "", 275 "footnoteReturnLinkContents": "", 276 "newContentEditor": "", 277 "paginate": 10, 278 "paginatePath": "page", 279 "summaryLength": 70, 280 "rssLimit": -1, 281 "sectionPagesMenu": "", 282 "disablePathToLower": false, 283 "hasCJKLanguage": false, 284 "enableEmoji": false, 285 "defaultContentLanguage": "en", 286 "defaultContentLanguageInSubdir": false, 287 "enableMissingTranslationPlaceholders": false, 288 "enableGitInfo": false, 289 "ignoreFiles": make([]string, 0), 290 "disableAliases": false, 291 "debug": false, 292 "disableFastRender": false, 293 "timeout": "30s", 294 "enableInlineShortcodes": false, 295 } 296 297 l.cfg.SetDefaults(defaultSettings) 298 299 return nil 300 } 301 302 func (l configLoader) applyOsEnvOverrides(environ []string) error { 303 if len(environ) == 0 { 304 return nil 305 } 306 307 const delim = "__env__delim" 308 309 // Extract all that start with the HUGO prefix. 310 // The delimiter is the following rune, usually "_". 311 const hugoEnvPrefix = "HUGO" 312 var hugoEnv []types.KeyValueStr 313 for _, v := range environ { 314 key, val := config.SplitEnvVar(v) 315 if strings.HasPrefix(key, hugoEnvPrefix) { 316 delimiterAndKey := strings.TrimPrefix(key, hugoEnvPrefix) 317 if len(delimiterAndKey) < 2 { 318 continue 319 } 320 // Allow delimiters to be case sensitive. 321 // It turns out there isn't that many allowed special 322 // chars in environment variables when used in Bash and similar, 323 // so variables on the form HUGOxPARAMSxFOO=bar is one option. 324 key := strings.ReplaceAll(delimiterAndKey[1:], delimiterAndKey[:1], delim) 325 key = strings.ToLower(key) 326 hugoEnv = append(hugoEnv, types.KeyValueStr{ 327 Key: key, 328 Value: val, 329 }) 330 331 } 332 } 333 334 for _, env := range hugoEnv { 335 existing, nestedKey, owner, err := maps.GetNestedParamFn(env.Key, delim, l.cfg.Get) 336 if err != nil { 337 return err 338 } 339 340 if existing != nil { 341 val, err := metadecoders.Default.UnmarshalStringTo(env.Value, existing) 342 if err != nil { 343 continue 344 } 345 346 if owner != nil { 347 owner[nestedKey] = val 348 } else { 349 l.cfg.Set(env.Key, val) 350 } 351 } else if nestedKey != "" { 352 owner[nestedKey] = env.Value 353 } else { 354 // The container does not exist yet. 355 l.cfg.Set(strings.ReplaceAll(env.Key, delim, "."), env.Value) 356 } 357 } 358 359 return nil 360 } 361 362 func (l configLoader) collectModules(modConfig modules.Config, v1 config.Provider, hookBeforeFinalize func(m *modules.ModulesConfig) error) (modules.Modules, []string, error) { 363 workingDir := l.WorkingDir 364 if workingDir == "" { 365 workingDir = v1.GetString("workingDir") 366 } 367 368 themesDir := paths.AbsPathify(l.WorkingDir, v1.GetString("themesDir")) 369 370 var ignoreVendor glob.Glob 371 if s := v1.GetString("ignoreVendorPaths"); s != "" { 372 ignoreVendor, _ = hglob.GetGlob(hglob.NormalizePath(s)) 373 } 374 375 filecacheConfigs, err := filecache.DecodeConfig(l.Fs, v1) 376 if err != nil { 377 return nil, nil, err 378 } 379 380 v1.Set("filecacheConfigs", filecacheConfigs) 381 382 var configFilenames []string 383 384 hook := func(m *modules.ModulesConfig) error { 385 for _, tc := range m.ActiveModules { 386 if len(tc.ConfigFilenames()) > 0 { 387 if tc.Watch() { 388 configFilenames = append(configFilenames, tc.ConfigFilenames()...) 389 } 390 391 // Merge from theme config into v1 based on configured 392 // merge strategy. 393 v1.Merge("", tc.Cfg().Get("")) 394 395 } 396 } 397 398 if hookBeforeFinalize != nil { 399 return hookBeforeFinalize(m) 400 } 401 402 return nil 403 } 404 405 modulesClient := modules.NewClient(modules.ClientConfig{ 406 Fs: l.Fs, 407 Logger: l.Logger, 408 HookBeforeFinalize: hook, 409 WorkingDir: workingDir, 410 ThemesDir: themesDir, 411 Environment: l.Environment, 412 CacheDir: filecacheConfigs.CacheDirModules(), 413 ModuleConfig: modConfig, 414 IgnoreVendor: ignoreVendor, 415 }) 416 417 v1.Set("modulesClient", modulesClient) 418 419 moduleConfig, err := modulesClient.Collect() 420 421 // Avoid recreating these later. 422 v1.Set("allModules", moduleConfig.ActiveModules) 423 424 if moduleConfig.GoModulesFilename != "" { 425 // We want to watch this for changes and trigger rebuild on version 426 // changes etc. 427 configFilenames = append(configFilenames, moduleConfig.GoModulesFilename) 428 } 429 430 return moduleConfig.ActiveModules, configFilenames, err 431 } 432 433 func (l configLoader) loadConfig(configName string) (string, error) { 434 baseDir := l.configFileDir() 435 var baseFilename string 436 if filepath.IsAbs(configName) { 437 baseFilename = configName 438 } else { 439 baseFilename = filepath.Join(baseDir, configName) 440 } 441 442 var filename string 443 if cpaths.ExtNoDelimiter(configName) != "" { 444 exists, _ := helpers.Exists(baseFilename, l.Fs) 445 if exists { 446 filename = baseFilename 447 } 448 } else { 449 for _, ext := range config.ValidConfigFileExtensions { 450 filenameToCheck := baseFilename + "." + ext 451 exists, _ := helpers.Exists(filenameToCheck, l.Fs) 452 if exists { 453 filename = filenameToCheck 454 break 455 } 456 } 457 } 458 459 if filename == "" { 460 return "", ErrNoConfigFile 461 } 462 463 m, err := config.FromFileToMap(l.Fs, filename) 464 if err != nil { 465 return "", l.wrapFileError(err, filename) 466 } 467 468 // Set overwrites keys of the same name, recursively. 469 l.cfg.Set("", m) 470 471 return filename, nil 472 } 473 474 func (l configLoader) deleteMergeStrategies() { 475 l.cfg.WalkParams(func(params ...config.KeyParams) bool { 476 params[len(params)-1].Params.DeleteMergeStrategy() 477 return false 478 }) 479 } 480 481 func (l configLoader) loadLanguageSettings(oldLangs langs.Languages) error { 482 _, err := langs.LoadLanguageSettings(l.cfg, oldLangs) 483 return err 484 } 485 486 func (l configLoader) loadModulesConfig() (modules.Config, error) { 487 modConfig, err := modules.DecodeConfig(l.cfg) 488 if err != nil { 489 return modules.Config{}, err 490 } 491 492 return modConfig, nil 493 } 494 495 func (configLoader) loadSiteConfig(cfg config.Provider) (scfg SiteConfig, err error) { 496 privacyConfig, err := privacy.DecodeConfig(cfg) 497 if err != nil { 498 return 499 } 500 501 servicesConfig, err := services.DecodeConfig(cfg) 502 if err != nil { 503 return 504 } 505 506 scfg.Privacy = privacyConfig 507 scfg.Services = servicesConfig 508 509 return 510 } 511 512 func (l configLoader) wrapFileError(err error, filename string) error { 513 return herrors.WithFileContextForFileDefault(err, filename, l.Fs) 514 }