diff options
Diffstat (limited to 'hugolib')
43 files changed, 3009 insertions, 3447 deletions
diff --git a/hugolib/config.go b/hugolib/config.go index 50e4ca6ec..b7ac46171 100644 --- a/hugolib/config.go +++ b/hugolib/config.go @@ -14,21 +14,24 @@ package hugolib import ( - "fmt" - "os" "path/filepath" "strings" + "github.com/gohugoio/hugo/common/loggers" + + "github.com/gohugoio/hugo/cache/filecache" + + "github.com/gohugoio/hugo/common/maps" + "github.com/gohugoio/hugo/parser/metadecoders" "github.com/gohugoio/hugo/common/herrors" "github.com/gohugoio/hugo/common/hugo" "github.com/gohugoio/hugo/hugolib/paths" - "github.com/pkg/errors" - _errors "github.com/pkg/errors" - "github.com/gohugoio/hugo/langs" + "github.com/gohugoio/hugo/modules" + "github.com/pkg/errors" "github.com/gohugoio/hugo/config" "github.com/gohugoio/hugo/config/privacy" @@ -67,7 +70,8 @@ func loadSiteConfig(cfg config.Provider) (scfg SiteConfig, err error) { // ConfigSourceDescriptor describes where to find the config (e.g. config.toml etc.). type ConfigSourceDescriptor struct { - Fs afero.Fs + Fs afero.Fs + Logger *loggers.Logger // Path to the config file to use, e.g. /my/project/config.toml Filename string @@ -84,6 +88,9 @@ type ConfigSourceDescriptor struct { // production, development Environment string + + // Defaults to os.Environ if not set. + Environ []string } func (d ConfigSourceDescriptor) configFilenames() []string { @@ -111,51 +118,43 @@ var ErrNoConfigFile = errors.New("Unable to locate config file or config directo // LoadConfig loads Hugo configuration into a new Viper and then adds // a set of defaults. func LoadConfig(d ConfigSourceDescriptor, doWithConfig ...func(cfg config.Provider) error) (*viper.Viper, []string, error) { + if d.Environment == "" { d.Environment = hugo.EnvironmentProduction } + if len(d.Environ) == 0 { + d.Environ = os.Environ() + } + var configFiles []string v := viper.New() l := configLoader{ConfigSourceDescriptor: d} - v.AutomaticEnv() - v.SetEnvPrefix("hugo") - - var cerr error - for _, name := range d.configFilenames() { var filename string - if filename, cerr = l.loadConfig(name, v); cerr != nil && cerr != ErrNoConfigFile { - return nil, nil, cerr + filename, err := l.loadConfig(name, v) + if err == nil { + configFiles = append(configFiles, filename) + } else if err != ErrNoConfigFile { + return nil, nil, err } - configFiles = append(configFiles, filename) } if d.AbsConfigDir != "" { dirnames, err := l.loadConfigFromConfigDir(v) if err == nil { configFiles = append(configFiles, dirnames...) + } else if err != ErrNoConfigFile { + return nil, nil, err } - cerr = err } if err := loadDefaultSettingsFor(v); err != nil { return v, configFiles, err } - if cerr == nil { - themeConfigFiles, err := l.loadThemeConfig(v) - if err != nil { - return v, configFiles, err - } - - if len(themeConfigFiles) > 0 { - configFiles = append(configFiles, themeConfigFiles...) - } - } - // We create languages based on the settings, so we need to make sure that // all configuration is loaded/set before doing that. for _, d := range doWithConfig { @@ -164,12 +163,75 @@ func LoadConfig(d ConfigSourceDescriptor, doWithConfig ...func(cfg config.Provid } } + // Apply environment overrides + if len(d.Environ) > 0 { + // Extract all that start with the HUGO_ prefix + const hugoEnvPrefix = "HUGO_" + var hugoEnv []string + for _, v := range d.Environ { + key, val := config.SplitEnvVar(v) + if strings.HasPrefix(key, hugoEnvPrefix) { + hugoEnv = append(hugoEnv, strings.ToLower(strings.TrimPrefix(key, hugoEnvPrefix)), val) + } + } + + if len(hugoEnv) > 0 { + for i := 0; i < len(hugoEnv); i += 2 { + key, valStr := strings.ToLower(hugoEnv[i]), hugoEnv[i+1] + + existing, nestedKey, owner, err := maps.GetNestedParamFn(key, "_", v.Get) + if err != nil { + return v, configFiles, err + } + + if existing != nil { + val, err := metadecoders.Default.UnmarshalStringTo(valStr, existing) + if err != nil { + continue + } + + if owner != nil { + owner[nestedKey] = val + } else { + v.Set(key, val) + } + } else { + v.Set(key, valStr) + } + } + } + } + + modulesConfig, err := l.loadModulesConfig(v) + if err != nil { + return v, configFiles, err + } + + mods, modulesConfigFiles, err := l.collectModules(modulesConfig, v) + if err != nil { + return v, configFiles, err + } + if err := loadLanguageSettings(v, nil); err != nil { return v, configFiles, err } - return v, configFiles, cerr + // Apply default project mounts. + if err := modules.ApplyProjectConfigDefaults(v, mods[len(mods)-1]); err != nil { + return v, configFiles, err + } + + if len(modulesConfigFiles) > 0 { + configFiles = append(configFiles, modulesConfigFiles...) + } + return v, configFiles, nil + +} + +func loadLanguageSettings(cfg config.Provider, oldLangs langs.Languages) error { + _, err := langs.LoadLanguageSettings(cfg, oldLangs) + return err } type configLoader struct { @@ -334,145 +396,79 @@ func (l configLoader) loadConfigFromConfigDir(v *viper.Viper) ([]string, error) return dirnames, nil } -func loadLanguageSettings(cfg config.Provider, oldLangs langs.Languages) error { +func (l configLoader) loadModulesConfig(v1 *viper.Viper) (modules.Config, error) { - defaultLang := cfg.GetString("defaultContentLanguage") - - var languages map[string]interface{} - - languagesFromConfig := cfg.GetStringMap("languages") - disableLanguages := cfg.GetStringSlice("disableLanguages") - - if len(disableLanguages) == 0 { - languages = languagesFromConfig - } else { - languages = make(map[string]interface{}) - for k, v := range languagesFromConfig { - for _, disabled := range disableLanguages { - if disabled == defaultLang { - return fmt.Errorf("cannot disable default language %q", defaultLang) - } - - if strings.EqualFold(k, disabled) { - v.(map[string]interface{})["disabled"] = true - break - } - } - languages[k] = v - } - } - - var ( - languages2 langs.Languages - err error - ) - - if len(languages) == 0 { - languages2 = append(languages2, langs.NewDefaultLanguage(cfg)) - } else { - languages2, err = toSortedLanguages(cfg, languages) - if err != nil { - return _errors.Wrap(err, "Failed to parse multilingual config") - } - } - - if oldLangs != nil { - // When in multihost mode, the languages are mapped to a server, so - // some structural language changes will need a restart of the dev server. - // The validation below isn't complete, but should cover the most - // important cases. - var invalid bool - if languages2.IsMultihost() != oldLangs.IsMultihost() { - invalid = true - } else { - if languages2.IsMultihost() && len(languages2) != len(oldLangs) { - invalid = true - } - } - - if invalid { - return errors.New("language change needing a server restart detected") - } - - if languages2.IsMultihost() { - // We need to transfer any server baseURL to the new language - for i, ol := range oldLangs { - nl := languages2[i] - nl.Set("baseURL", ol.GetString("baseURL")) - } - } + modConfig, err := modules.DecodeConfig(v1) + if err != nil { + return modules.Config{}, err } - // The defaultContentLanguage is something the user has to decide, but it needs - // to match a language in the language definition list. - langExists := false - for _, lang := range languages2 { - if lang.Lang == defaultLang { - langExists = true - break - } - } + return modConfig, nil +} - if !langExists { - return fmt.Errorf("site config value %q for defaultContentLanguage does not match any language definition", defaultLang) +func (l configLoader) collectModules(modConfig modules.Config, v1 *viper.Viper) (modules.Modules, []string, error) { + workingDir := l.WorkingDir + if workingDir == "" { + workingDir = v1.GetString("workingDir") } - cfg.Set("languagesSorted", languages2) - cfg.Set("multilingual", len(languages2) > 1) + themesDir := paths.AbsPathify(l.WorkingDir, v1.GetString("themesDir")) - multihost := languages2.IsMultihost() - - if multihost { - cfg.Set("defaultContentLanguageInSubdir", true) - cfg.Set("multihost", true) - } - - if multihost { - // The baseURL may be provided at the language level. If that is true, - // then every language must have a baseURL. In this case we always render - // to a language sub folder, which is then stripped from all the Permalink URLs etc. - for _, l := range languages2 { - burl := l.GetLocal("baseURL") - if burl == nil { - return errors.New("baseURL must be set on all or none of the languages") - } - } + ignoreVendor := v1.GetBool("ignoreVendor") + filecacheConfigs, err := filecache.DecodeConfig(l.Fs, v1) + if err != nil { + return nil, nil, err } + v1.Set("filecacheConfigs", filecacheConfigs) - return nil -} + modulesClient := modules.NewClient(modules.ClientConfig{ + Fs: l.Fs, + Logger: l.Logger, + WorkingDir: workingDir, + ThemesDir: themesDir, + CacheDir: filecacheConfigs.CacheDirModules(), + ModuleConfig: modConfig, + IgnoreVendor: ignoreVendor, + }) -func (l configLoader) loadThemeConfig(v1 *viper.Viper) ([]string, error) { - themesDir := paths.AbsPathify(l.WorkingDir, v1.GetString("themesDir")) - themes := config.GetStringSlicePreserveString(v1, "theme") + v1.Set("modulesClient", modulesClient) - themeConfigs, err := paths.CollectThemes(l.Fs, themesDir, themes) + moduleConfig, err := modulesClient.Collect() if err != nil { - return nil, err + return nil, nil, err } - if len(themeConfigs) == 0 { - return nil, nil - } + // Avoid recreating these later. + v1.Set("allModules", moduleConfig.ActiveModules) - v1.Set("allThemes", themeConfigs) + if len(moduleConfig.ActiveModules) == 0 { + return nil, nil, nil + } var configFilenames []string - for _, tc := range themeConfigs { - if tc.ConfigFilename != "" { - configFilenames = append(configFilenames, tc.ConfigFilename) + for _, tc := range moduleConfig.ActiveModules { + if tc.ConfigFilename() != "" { + if tc.Watch() { + configFilenames = append(configFilenames, tc.ConfigFilename()) + } if err := l.applyThemeConfig(v1, tc); err != nil { - return nil, err + return nil, nil, err } } } - return configFilenames, nil + if moduleConfig.GoModulesFilename != "" { + // We want to watch this for changes and trigger rebuild on version + // changes etc. + configFilenames = append(configFilenames, moduleConfig.GoModulesFilename) + } + + return moduleConfig.ActiveModules, configFilenames, nil } -func (l configLoader) applyThemeConfig(v1 *viper.Viper, theme paths.ThemeConfig) error { +func (l configLoader) applyThemeConfig(v1 *viper.Viper, theme modules.Module) error { const ( paramsKey = "params" @@ -480,22 +476,12 @@ func (l configLoader) applyThemeConfig(v1 *viper.Viper, theme paths.ThemeConfig) menuKey = "menus" ) - v2 := theme.Cfg + v2 := theme.Cfg() for _, key := range []string{paramsKey, "outputformats", "mediatypes"} { l.mergeStringMapKeepLeft("", key, v1, v2) } - themeLower := strings.ToLower(theme.Name) - themeParamsNamespace := paramsKey + "." + themeLower - - // Set namespaced params - if v2.IsSet(paramsKey) && !v1.IsSet(themeParamsNamespace) { - // Set it in the default store to make sure it gets in the same or - // behind the others. - v1.SetDefault(themeParamsNamespace, v2.Get(paramsKey)) - } - // Only add params and new menu entries, we do not add language definitions. if v1.IsSet(languagesKey) && v2.IsSet(languagesKey) { v1Langs := v1.GetStringMap(languagesKey) @@ -508,12 +494,6 @@ func (l configLoader) applyThemeConfig(v1 *viper.Viper, theme paths.ThemeConfig) if k == "" { continue } - langParamsKey := languagesKey + "." + k + "." + paramsKey - langParamsThemeNamespace := langParamsKey + "." + themeLower - // Set namespaced params - if v2.IsSet(langParamsKey) && !v1.IsSet(langParamsThemeNamespace) { - v1.SetDefault(langParamsThemeNamespace, v2.Get(langParamsKey)) - } langMenuKey := languagesKey + "." + k + "." + menuKey if v2.IsSet(langMenuKey) { @@ -577,18 +557,23 @@ func loadDefaultSettingsFor(v *viper.Viper) error { v.RegisterAlias("indexes", "taxonomies") + /* + + TODO(bep) from 0.56 these are configured as module mounts. + v.SetDefault("contentDir", "content") + v.SetDefault("layoutDir", "layouts") + v.SetDefault("assetDir", "assets") + v.SetDefault("staticDir", "static") + v.SetDefault("dataDir", "data") + v.SetDefault("i18nDir", "i18n") + v.SetDefault("archetypeDir", "archetypes") + */ + v.SetDefault("cleanDestinationDir", false) v.SetDefault("watch", false) v.SetDefault("metaDataFormat", "toml") - v.SetDefault("contentDir", "content") - v.SetDefault("layoutDir", "layouts") - v.SetDefault("assetDir", "assets") - v.SetDefault("staticDir", "static") v.SetDefault("resourceDir", "resources") - v.SetDefault("archetypeDir", "archetypes") v.SetDefault("publishDir", "public") - v.SetDefault("dataDir", "data") - v.SetDefault("i18nDir", "i18n") v.SetDefault("themesDir", "themes") v.SetDefault("buildDrafts", false) v.SetDefault("buildFuture", false) @@ -635,5 +620,6 @@ func loadDefaultSettingsFor(v *viper.Viper) error { v.SetDefault("disableFastRender", false) v.SetDefault("timeout", 10000) // 10 seconds v.SetDefault("enableInlineShortcodes", false) + return nil } diff --git a/hugolib/config_test.go b/hugolib/config_test.go index 885a07ee9..bd980235f 100644 --- a/hugolib/config_test.go +++ b/hugolib/config_test.go @@ -14,6 +14,9 @@ package hugolib import ( + "bytes" + "fmt" + "path/filepath" "testing" "github.com/spf13/afero" @@ -40,10 +43,7 @@ func TestLoadConfig(t *testing.T) { require.NoError(t, err) assert.Equal("side", cfg.GetString("paginatePath")) - // default - assert.Equal("layouts", cfg.GetString("layoutDir")) - // no themes - assert.False(cfg.IsSet("allThemes")) + } func TestLoadMultiConfig(t *testing.T) { @@ -188,11 +188,6 @@ map[string]interface {}{ "p1": "p1 main", "p2": "p2 main", "p3": "p3 theme", - "test-theme": map[string]interface {}{ - "p1": "p1 theme", - "p2": "p2 theme", - "p3": "p3 theme", - }, "top": "top", }`, got["params"]) @@ -257,10 +252,6 @@ map[string]interface {}{ "params": map[string]interface {}{ "pl1": "p1-en-main", "pl2": "p2-en-theme", - "test-theme": map[string]interface {}{ - "pl1": "p1-en-theme", - "pl2": "p2-en-theme", - }, }, }, "nb": map[string]interface {}{ @@ -275,11 +266,6 @@ map[string]interface {}{ "params": map[string]interface {}{ "pl1": "p1-nb-main", "pl2": "p2-nb-theme", - "test-theme": map[string]interface {}{ - "pl1": "p1-nb-theme", - "pl2": "p2-nb-theme", - "top": "top-nb-theme", - }, }, }, } @@ -397,3 +383,142 @@ privacyEnhanced = true assert.True(b.H.Sites[0].Info.Config().Privacy.YouTube.PrivacyEnhanced) } + +func TestLoadConfigModules(t *testing.T) { + t.Parallel() + + assert := require.New(t) + + // https://github.com/gohugoio/hugoThemes#themetoml + + const ( + // Before Hugo 0.56 each theme/component could have its own theme.toml + // with some settings, mostly used on the Hugo themes site. + // To preserve combability we read these files into the new "modules" + // section in config.toml. + o1t = ` +name = "Component o1" +license = "MIT" +min_version = 0.38 +` + // This is the component's config.toml, using the old theme syntax. + o1c = ` +theme = ["n2"] +` + + n1 = ` +title = "Component n1" + +[module] +description = "Component n1 description" +[module.hugoVersion] +min = "0.40.0" +max = "0.50.0" +extended = true +[[module.imports]] +path="o1" +[[module.imports]] +path="n3" + + +` + + n2 = ` +title = "Component n2" +` + + n3 = ` +title = "Component n3" +` + + n4 = ` +title = "Component n4" +` + ) + + b := newTestSitesBuilder(t) + + writeThemeFiles := func(name, configTOML, themeTOML string) { + b.WithSourceFile(filepath.Join("themes", name, "data", "module.toml"), fmt.Sprintf("name=%q", name)) + if configTOML != "" { + b.WithSourceFile(filepath.Join("themes", name, "config.toml"), configTOML) + } + if themeTOML != "" { + b.WithSourceFile(filepath.Join("themes", name, "theme.toml"), themeTOML) + } + } + + writeThemeFiles("n1", n1, "") + writeThemeFiles("n2", n2, "") + writeThemeFiles("n3", n3, "") + writeThemeFiles("n4", n4, "") + writeThemeFiles("o1", o1c, o1t) + + b.WithConfigFile("toml", ` +[module] +[[module.imports]] +path="n1" +[[module.imports]] +path="n4" + +`) + + b.Build(BuildCfg{}) + + modulesClient := b.H.Paths.ModulesClient + var graphb bytes.Buffer + modulesClient.Graph(&graphb) + + assert.Equal(`project n1 +n1 o1 +o1 n2 +n1 n3 +project n4 +`, graphb.String()) + +} + +func TestLoadConfigWithOsEnvOverrides(t *testing.T) { + + assert := require.New(t) + + baseConfig := ` + +environment = "production" +enableGitInfo = true +intSlice = [5,7,9] +floatSlice = [3.14, 5.19] +stringSlice = ["a", "b"] + +[imaging] +anchor = "smart" +quality = 75 +resamplefilter = "CatmullRom" +` + + b := newTestSitesBuilder(t).WithConfigFile("toml", baseConfig) + + b.WithEnviron( + "HUGO_ENVIRONMENT", "test", + "HUGO_NEW", "new", // key not in config.toml + "HUGO_ENABLEGITINFO", "false", + "HUGO_IMAGING_ANCHOR", "top", + "HUGO_STRINGSLICE", `["c", "d"]`, + "HUGO_INTSLICE", `[5, 8, 9]`, + "HUGO_FLOATSLICE", `[5.32]`, + ) + + b.Build(BuildCfg{}) + + cfg := b.H.Cfg + + assert.Equal("test", cfg.Get("environment")) + assert.Equal(false, cfg.GetBool("enablegitinfo")) + assert.Equal("new", cfg.Get("new")) + assert.Equal("top", cfg.Get("imaging.anchor")) + assert.Equal(int64(75), cfg.Get("imaging.quality")) + assert.Equal([]interface{}{"c", "d"}, cfg.Get("stringSlice")) + assert.Equal([]interface{}{5.32}, cfg.Get("floatSlice")) + assert.Equal([]interface{}{5, 8, 9}, cfg.Get("intSlice")) + +} diff --git a/hugolib/data/hugo.toml b/hugolib/data/hugo.toml new file mo |