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