summaryrefslogtreecommitdiffstats
path: root/modules/collect.go
diff options
context:
space:
mode:
Diffstat (limited to 'modules/collect.go')
-rw-r--r--modules/collect.go574
1 files changed, 574 insertions, 0 deletions
diff --git a/modules/collect.go b/modules/collect.go
new file mode 100644
index 000000000..f57b4d04b
--- /dev/null
+++ b/modules/collect.go
@@ -0,0 +1,574 @@
+// Copyright 2019 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package modules
+
+import (
+ "bufio"
+ "fmt"
+ "os"
+ "path/filepath"
+ "strings"
+
+ "github.com/spf13/cast"
+
+ "github.com/gohugoio/hugo/common/maps"
+
+ "github.com/gohugoio/hugo/common/hugo"
+ "github.com/gohugoio/hugo/parser/metadecoders"
+
+ "github.com/gohugoio/hugo/hugofs/files"
+
+ "github.com/rogpeppe/go-internal/module"
+
+ "github.com/pkg/errors"
+
+ "github.com/gohugoio/hugo/config"
+ "github.com/spf13/afero"
+)
+
+var ErrNotExist = errors.New("module does not exist")
+
+const vendorModulesFilename = "modules.txt"
+
+// IsNotExist returns whether an error means that a module could not be found.
+func IsNotExist(err error) bool {
+ return errors.Cause(err) == ErrNotExist
+}
+
+// CreateProjectModule creates modules from the given config.
+// This is used in tests only.
+func CreateProjectModule(cfg config.Provider) (Module, error) {
+ workingDir := cfg.GetString("workingDir")
+ var modConfig Config
+
+ mod := createProjectModule(nil, workingDir, modConfig)
+ if err := ApplyProjectConfigDefaults(cfg, mod); err != nil {
+ return nil, err
+ }
+
+ return mod, nil
+}
+
+func (h *Client) Collect() (ModulesConfig, error) {
+ mc, coll := h.collect(true)
+ return mc, coll.err
+
+}
+
+func (h *Client) collect(tidy bool) (ModulesConfig, *collector) {
+ c := &collector{
+ Client: h,
+ }
+
+ c.collect()
+ if c.err != nil {
+ return ModulesConfig{}, c
+ }
+
+ if !c.skipTidy && tidy {
+ if err := h.tidy(c.modules, true); err != nil {
+ c.err = err
+ return ModulesConfig{}, c
+ }
+ }
+
+ // TODO(bep) consider --ignoreVendor vs removing from go.mod
+ var activeMods Modules
+ for _, mod := range c.modules {
+ if !mod.Config().HugoVersion.IsValid() {
+ h.logger.WARN.Printf(`Module %q is not compatible with this Hugo version; run "hugo mod graph" for more information.`, mod.Path())
+ }
+ if !mod.Disabled() {
+ activeMods = append(activeMods, mod)
+ }
+ }
+
+ return ModulesConfig{
+ AllModules: c.modules,
+ ActiveModules: activeMods,
+ GoModulesFilename: c.GoModulesFilename,
+ }, c
+
+}
+
+type ModulesConfig struct {
+ // All modules, including any disabled.
+ AllModules Modules
+
+ // All active modules.
+ ActiveModules Modules
+
+ // Set if this is a Go modules enabled project.
+ GoModulesFilename string
+}
+
+type collected struct {
+ // Pick the first and prevent circular loops.
+ seen map[string]bool
+
+ // Maps module path to a _vendor dir. These values are fetched from
+ // _vendor/modules.txt, and the first (top-most) will win.
+ vendored map[string]vendoredModule
+
+ // Set if a Go modules enabled project.
+ gomods goModules
+
+ // Ordered list of collected modules, including Go Modules and theme
+ // components stored below /themes.
+ modules Modules
+}
+
+// Collects and creates a module tree.
+type collector struct {
+ *Client
+
+ // Store away any non-fatal error and return at the end.
+ err error
+
+ // Set to disable any Tidy operation in the end.
+ skipTidy bool
+
+ *collected
+}
+
+func (c *collector) initModules() error {
+ c.collected = &collected{
+ seen: make(map[string]bool),
+ vendored: make(map[string]vendoredModule),
+ }
+
+ // We may fail later if we don't find the mods.
+ return c.loadModules()
+}
+
+func (c *collector) isSeen(path string) bool {
+ key := pathKey(path)
+ if c.seen[key] {
+ return true
+ }
+ c.seen[key] = true
+ return false
+}
+
+func (c *collector) getVendoredDir(path string) (vendoredModule, bool) {
+ v, found := c.vendored[path]
+ return v, found
+}
+
+func (c *collector) add(owner *moduleAdapter, moduleImport Import, disabled bool) (*moduleAdapter, error) {
+ var (
+ mod *goModule
+ moduleDir string
+ version string
+ vendored bool
+ )
+
+ modulePath := moduleImport.Path
+ var realOwner Module = owner
+
+ if !c.ignoreVendor {
+ if err := c.collectModulesTXT(owner); err != nil {
+ return nil, err
+ }
+
+ // Try _vendor first.
+ var vm vendoredModule
+ vm, vendored = c.getVendoredDir(modulePath)
+ if vendored {
+ moduleDir = vm.Dir
+ realOwner = vm.Owner
+ version = vm.Version
+
+ if owner.projectMod {
+ // We want to keep the go.mod intact with the versions and all.
+ c.skipTidy = true
+ }
+
+ }
+ }
+
+ if moduleDir == "" {
+ mod = c.gomods.GetByPath(modulePath)
+ if mod != nil {
+ moduleDir = mod.Dir
+ }
+
+ if moduleDir == "" {
+
+ if c.GoModulesFilename != "" && c.isProbablyModule(modulePath) {
+ // Try to "go get" it and reload the module configuration.
+ if err := c.Get(modulePath); err != nil {
+ return nil, err
+ }
+ if err := c.loadModules(); err != nil {
+ return nil, err
+ }
+
+ mod = c.gomods.GetByPath(modulePath)
+ if mod != nil {
+ moduleDir = mod.Dir
+ }
+ }
+
+ // Fall back to /themes/<mymodule>
+ if moduleDir == "" {
+ moduleDir = filepath.Join(c.themesDir, modulePath)
+
+ if found, _ := afero.Exists(c.fs, moduleDir); !found {
+ c.err = c.wrapModuleNotFound(errors.Errorf(`module %q not found; either add it as a Hugo Module or store it in %q.`, modulePath, c.themesDir))
+ return nil, nil
+ }
+ }
+ }
+ }
+
+ if found, _ := afero.Exists(c.fs, moduleDir); !found {
+ c.err = c.wrapModuleNotFound(errors.Errorf("%q not found", moduleDir))
+ return nil, nil
+ }
+
+ if !strings.HasSuffix(moduleDir, fileSeparator) {
+ moduleDir += fileSeparator
+ }
+
+ ma := &moduleAdapter{
+ dir: moduleDir,
+ vendor: vendored,
+ disabled: disabled,
+ gomod: mod,
+ version: version,
+ // This may be the owner of the _vendor dir
+ owner: realOwner,
+ }
+
+ if mod == nil {
+ ma.path = modulePath
+ }
+
+ if err := ma.validateAndApplyDefaults(c.fs); err != nil {
+ return nil, err
+ }
+
+ if !moduleImport.IgnoreConfig {
+ if err := c.applyThemeConfig(ma); err != nil {
+ return nil, err
+ }
+ }
+
+ if err := c.applyMounts(moduleImport, ma); err != nil {
+ return nil, err
+ }
+
+ c.modules = append(c.modules, ma)
+ return ma, nil
+
+}
+
+func (c *collector) addAndRecurse(owner *moduleAdapter, disabled bool) error {
+ moduleConfig := owner.Config()
+ if owner.projectMod {
+ if err := c.applyMounts(Import{}, owner); err != nil {
+ return err
+ }
+ }
+
+ for _, moduleImport := range moduleConfig.Imports {
+ disabled := disabled || moduleImport.Disabled
+
+ if !c.isSeen(moduleImport.Path) {
+ tc, err := c.add(owner, moduleImport, disabled)
+ if err != nil {
+ return err
+ }
+ if tc == nil {
+ continue
+ }
+ if err := c.addAndRecurse(tc, disabled); err != nil {
+ return err
+ }
+ }
+ }
+ return nil
+}
+
+func (c *collector) applyMounts(moduleImport Import, mod *moduleAdapter) error {
+ mounts := moduleImport.Mounts
+
+ if !mod.projectMod && len(mounts) == 0 {
+ modConfig := mod.Config()
+ mounts = modConfig.Mounts
+ if len(mounts) == 0 {
+ // Create default mount points for every component folder that
+ // exists in the module.
+ for _, componentFolder := range files.ComponentFolders {
+ sourceDir := filepath.Join(mod.Dir(), componentFolder)
+ _, err := c.fs.Stat(sourceDir)
+ if err == nil {
+ mounts = append(mounts, Mount{
+ Source: componentFolder,
+ Target: componentFolder,
+ })
+ }
+ }
+ }
+ }
+
+ var err error
+ mounts, err = c.normalizeMounts(mod, mounts)
+ if err != nil {
+ return err
+ }
+
+ mod.mounts = mounts
+ return nil
+}
+
+func (c *collector) applyThemeConfig(tc *moduleAdapter) error {
+
+ var (
+ configFilename string
+ cfg config.Provider
+ themeCfg map[string]interface{}
+ hasConfig bool
+ err error
+ )
+
+ // Viper supports more, but this is the sub-set supported by Hugo.
+ for _, configFormats := range config.ValidConfigFileExtensions {
+ configFilename = filepath.Join(tc.Dir(), "config."+configFormats)
+ hasConfig, _ = afero.Exists(c.fs, configFilename)
+ if hasConfig {
+ break
+ }
+ }
+
+ // The old theme information file.
+ themeTOML := filepath.Join(tc.Dir(), "theme.toml")
+
+ hasThemeTOML, _ := afero.Exists(c.fs, themeTOML)
+ if hasThemeTOML {
+ data, err := afero.ReadFile(c.fs, themeTOML)
+ if err != nil {
+ return err
+ }
+ themeCfg, err = metadecoders.Default.UnmarshalToMap(data, metadecoders.TOML)
+ if err != nil {
+ return errors.Wrapf(err, "failed to read module config for %q in %q", tc.Path(), themeTOML)
+ }
+ maps.ToLower(themeCfg)
+ }
+
+ if hasConfig {
+ if configFilename != "" {
+ var err error
+ cfg, err = config.FromFile(c.fs, configFilename)
+ if err != nil {
+ return errors.Wrapf(err, "failed to read module config for %q in %q", tc.Path(), configFilename)
+ }
+ }
+
+ tc.configFilename = configFilename
+ tc.cfg = cfg
+ }
+
+ config, err := DecodeConfig(cfg)
+ if err != nil {
+ return err
+ }
+
+ const oldVersionKey = "min_version"
+
+ if hasThemeTOML {
+
+ // Merge old with new
+ if minVersion, found := themeCfg[oldVersionKey]; found {
+ if config.HugoVersion.Min == "" {
+ config.HugoVersion.Min = hugo.VersionString(cast.ToString(minVersion))
+ }
+ }
+
+ if config.Params == nil {
+ config.Params = make(map[string]interface{})
+ }
+
+ for k, v := range themeCfg {
+ if k == oldVersionKey {
+ continue
+ }
+ config.Params[k] = v
+ }
+
+ }
+
+ tc.config = config
+
+ return nil
+
+}
+
+func (c *collector) collect() {
+ if err := c.initModules(); err != nil {
+ c.err = err
+ return
+ }
+
+ projectMod := createProjectModule(c.gomods.GetMain(), c.workingDir, c.moduleConfig)
+
+ if err := c.addAndRecurse(projectMod, false); err != nil {
+ c.err = err
+ return
+ }
+
+ // Append the project module at the tail.
+ c.modules = append(c.modules, projectMod)
+
+}
+
+func (c *collector) collectModulesTXT(owner Module) error {
+ vendorDir := filepath.Join(owner.Dir(), vendord)
+ filename := filepath.Join(vendorDir, vendorModulesFilename)
+
+ f, err := c.fs.Open(filename)
+
+ if err != nil {
+ if os.IsNotExist(err) {
+ return nil
+ }
+
+ return err
+ }
+
+ defer f.Close()
+
+ scanner := bufio.NewScanner(f)
+
+ for scanner.Scan() {
+ // # github.com/alecthomas/chroma v0.6.3
+ line := scanner.Text()
+ line = strings.Trim(line, "# ")
+ line = strings.TrimSpace(line)
+ parts := strings.Fields(line)
+ if len(parts) != 2 {
+ return errors.Errorf("invalid modules list: %q", filename)
+ }
+ path := parts[0]
+ if _, found := c.vendored[path]; !found {
+ c.vendored[path] = vendoredModule{
+ Owner: owner,
+ Dir: filepath.Join(vendorDir, path),
+ Version: parts[1],
+ }
+ }
+
+ }
+ return nil
+}
+
+func (c *collector) loadModules() error {
+ modules, err := c.listGoMods()
+ if err != nil {
+ return err
+ }
+ c.gomods = modules
+ return nil
+}
+
+func (c *collector) normalizeMounts(owner Module, mounts []Mount) ([]Mount, error) {
+ var out []Mount
+ dir := owner.Dir()
+
+ for _, mnt := range mounts {
+ errMsg := fmt.Sprintf("invalid module config for %q", owner.Path())
+
+ if mnt.Source == "" || mnt.Target == "" {
+ return nil, errors.New(errMsg + ": both source and target must be set")
+ }
+
+ mnt.Source = filepath.Clean(mnt.Source)
+ mnt.Target = filepath.Clean(mnt.Target)
+
+ // Verify that Source exists
+ sourceDir := filepath.Join(dir, mnt.Source)
+ _, err := c.fs.Stat(sourceDir)
+ if err != nil {
+ continue
+ }
+
+ // Verify that target points to one of the predefined component dirs
+ targetBase := mnt.Target
+ idxPathSep := strings.Index(mnt.Target, string(os.PathSeparator))
+ if idxPathSep != -1 {
+ targetBase = mnt.Target[0:idxPathSep]
+ }
+ if !files.IsComponentFolder(targetBase) {
+ return nil, errors.Errorf("%s: mount target must be one of: %v", errMsg, files.ComponentFolders)
+ }
+
+ out = append(out, mnt)
+ }
+
+ return out, nil
+}
+
+func (c *collector) wrapModuleNotFound(err error) error {
+ err = errors.Wrap(ErrNotExist, err.Error())
+ if c.GoModulesFilename == "" {
+ return err
+ }
+
+ baseMsg := "we found a go.mod file in your project, but"
+
+ switch c.goBinaryStatus {
+ case goBinaryStatusNotFound:
+ return errors.Wrap(err, baseMsg+" you need to install Go to use it. See https://golang.org/dl/.")
+ case goBinaryStatusTooOld:
+ return errors.Wrap(err, baseMsg+" you need to a newer version of Go to use it. See https://golang.org/dl/.")
+ }
+
+ return err
+
+}
+
+type vendoredModule struct {
+ Owner Module
+ Dir string
+ Version string
+}
+
+func createProjectModule(gomod *goModule, workingDir string, conf Config) *moduleAdapter {
+ // Create a pseudo module for the main project.
+ var path string
+ if gomod == nil {
+ path = "project"
+ }
+
+ return &moduleAdapter{
+ path: path,
+ dir: workingDir,
+ gomod: gomod,
+ projectMod: true,
+ config: conf,
+ }
+
+}
+
+// In the first iteration of Hugo Modules, we do not support multiple
+// major versions running at the same time, so we pick the first (upper most).
+// We will investigate namespaces in future versions.
+// TODO(bep) add a warning when the above happens.
+func pathKey(p string) string {
+ prefix, _, _ := module.SplitPathVersion(p)
+
+ return strings.ToLower(prefix)
+}