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