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