summaryrefslogtreecommitdiffstats
path: root/modules
diff options
context:
space:
mode:
Diffstat (limited to 'modules')
-rw-r--r--modules/client.go570
-rw-r--r--modules/client_test.go117
-rw-r--r--modules/collect.go574
-rw-r--r--modules/collect_test.go38
-rw-r--r--modules/config.go335
-rw-r--r--modules/config_test.go132
-rw-r--r--modules/module.go196
7 files changed, 1962 insertions, 0 deletions
diff --git a/modules/client.go b/modules/client.go
new file mode 100644
index 000000000..ac09721dc
--- /dev/null
+++ b/modules/client.go
@@ -0,0 +1,570 @@
+// 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"
+ "bytes"
+ "context"
+ "encoding/json"
+ "fmt"
+ "io"
+ "io/ioutil"
+ "os"
+ "os/exec"
+ "path/filepath"
+
+ "github.com/gohugoio/hugo/hugofs/files"
+
+ "github.com/gohugoio/hugo/common/loggers"
+
+ "strings"
+ "time"
+
+ "github.com/gohugoio/hugo/config"
+
+ "github.com/rogpeppe/go-internal/module"
+
+ "github.com/gohugoio/hugo/common/hugio"
+
+ "github.com/pkg/errors"
+ "github.com/spf13/afero"
+)
+
+var (
+ fileSeparator = string(os.PathSeparator)
+)
+
+const (
+ goBinaryStatusOK goBinaryStatus = iota
+ goBinaryStatusNotFound
+ goBinaryStatusTooOld
+)
+
+// The "vendor" dir is reserved for Go Modules.
+const vendord = "_vendor"
+
+const (
+ goModFilename = "go.mod"
+ goSumFilename = "go.sum"
+)
+
+// NewClient creates a new Client that can be used to manage the Hugo Components
+// in a given workingDir.
+// The Client will resolve the dependencies recursively, but needs the top
+// level imports to start out.
+func NewClient(cfg ClientConfig) *Client {
+ fs := cfg.Fs
+
+ n := filepath.Join(cfg.WorkingDir, goModFilename)
+ goModEnabled, _ := afero.Exists(fs, n)
+ var goModFilename string
+ if goModEnabled {
+ goModFilename = n
+ }
+
+ env := os.Environ()
+ mcfg := cfg.ModuleConfig
+
+ config.SetEnvVars(&env,
+ "PWD", cfg.WorkingDir,
+ "GOPROXY", mcfg.Proxy,
+ "GOPRIVATE", mcfg.Private,
+ "GONOPROXY", mcfg.NoProxy)
+
+ if cfg.CacheDir != "" {
+ // Module cache stored below $GOPATH/pkg
+ config.SetEnvVars(&env, "GOPATH", cfg.CacheDir)
+
+ }
+
+ logger := cfg.Logger
+ if logger == nil {
+ logger = loggers.NewWarningLogger()
+ }
+
+ return &Client{
+ fs: fs,
+ ignoreVendor: cfg.IgnoreVendor,
+ workingDir: cfg.WorkingDir,
+ themesDir: cfg.ThemesDir,
+ logger: logger,
+ moduleConfig: mcfg,
+ environ: env,
+ GoModulesFilename: goModFilename}
+}
+
+// Client contains most of the API provided by this package.
+type Client struct {
+ fs afero.Fs
+ logger *loggers.Logger
+
+ // Ignore any _vendor directory.
+ ignoreVendor bool
+
+ // Absolute path to the project dir.
+ workingDir string
+
+ // Absolute path to the project's themes dir.
+ themesDir string
+
+ // The top level module config
+ moduleConfig Config
+
+ // Environment variables used in "go get" etc.
+ environ []string
+
+ // Set when Go modules are initialized in the current repo, that is:
+ // a go.mod file exists.
+ GoModulesFilename string
+
+ // Set if we get a exec.ErrNotFound when running Go, which is most likely
+ // due to being run on a system without Go installed. We record it here
+ // so we can give an instructional error at the end if module/theme
+ // resolution fails.
+ goBinaryStatus goBinaryStatus
+}
+
+// Graph writes a module dependenchy graph to the given writer.
+func (c *Client) Graph(w io.Writer) error {
+ mc, coll := c.collect(true)
+ if coll.err != nil {
+ return coll.err
+ }
+ for _, module := range mc.AllModules {
+ if module.Owner() == nil {
+ continue
+ }
+
+ prefix := ""
+ if module.Disabled() {
+ prefix = "DISABLED "
+ }
+ dep := pathVersion(module.Owner()) + " " + pathVersion(module)
+ if replace := module.Replace(); replace != nil {
+ if replace.Version() != "" {
+ dep += " => " + pathVersion(replace)
+ } else {
+ // Local dir.
+ dep += " => " + replace.Dir()
+ }
+
+ }
+ fmt.Fprintln(w, prefix+dep)
+ }
+
+ return nil
+}
+
+// Tidy can be used to remove unused dependencies from go.mod and go.sum.
+func (c *Client) Tidy() error {
+ tc, coll := c.collect(false)
+ if coll.err != nil {
+ return coll.err
+ }
+
+ if coll.skipTidy {
+ return nil
+ }
+
+ return c.tidy(tc.AllModules, false)
+}
+
+// Vendor writes all the module dependencies to a _vendor folder.
+//
+// Unlike Go, we support it for any level.
+//
+// We, by default, use the /_vendor folder first, if found. To disable,
+// run with
+// hugo --ignoreVendor
+//
+// Given a module tree, Hugo will pick the first module for a given path,
+// meaning that if the top-level module is vendored, that will be the full
+// set of dependencies.
+func (c *Client) Vendor() error {
+ vendorDir := filepath.Join(c.workingDir, vendord)
+ if err := c.rmVendorDir(vendorDir); err != nil {
+ return err
+ }
+
+ // Write the modules list to modules.txt.
+ //
+ // On the form:
+ //
+ // # github.com/alecthomas/chroma v0.6.3
+ //
+ // This is how "go mod vendor" does it. Go also lists
+ // the packages below it, but that is currently not applicable to us.
+ //
+ var modulesContent bytes.Buffer
+
+ tc, coll := c.collect(true)
+ if coll.err != nil {
+ return coll.err
+ }
+
+ for _, t := range tc.AllModules {
+ if t.Owner() == nil {
+ // This is the project.
+ continue
+ }
+ // We respect the --ignoreVendor flag even for the vendor command.
+ if !t.IsGoMod() && !t.Vendor() {
+ // We currently do not vendor components living in the
+ // theme directory, see https://github.com/gohugoio/hugo/issues/5993
+ continue
+ }
+
+ fmt.Fprintln(&modulesContent, "# "+t.Path()+" "+t.Version())
+
+ dir := t.Dir()
+
+ for _, mount := range t.Mounts() {
+ if err := hugio.CopyDir(c.fs, filepath.Join(dir, mount.Source), filepath.Join(vendorDir, t.Path(), mount.Source), nil); err != nil {
+ return errors.Wrap(err, "failed to copy module to vendor dir")
+ }
+ }
+
+ // Include the resource cache if present.
+ resourcesDir := filepath.Join(dir, files.FolderResources)
+ _, err := c.fs.Stat(resourcesDir)
+ if err == nil {
+ if err := hugio.CopyDir(c.fs, resourcesDir, filepath.Join(vendorDir, t.Path(), files.FolderResources), nil); err != nil {
+ return errors.Wrap(err, "failed to copy resources to vendor dir")
+ }
+ }
+
+ // Also include any theme.toml or config.* files in the root.
+ configFiles, _ := afero.Glob(c.fs, filepath.Join(dir, "config.*"))
+ configFiles = append(configFiles, filepath.Join(dir, "theme.toml"))
+ for _, configFile := range configFiles {
+ if err := hugio.CopyFile(c.fs, configFile, filepath.Join(vendorDir, t.Path(), filepath.Base(configFile))); err != nil {
+ if !os.IsNotExist(err) {
+ return err
+ }
+ }
+ }
+ }
+
+ if modulesContent.Len() > 0 {
+ if err := afero.WriteFile(c.fs, filepath.Join(vendorDir, vendorModulesFilename), modulesContent.Bytes(), 0666); err != nil {
+ return err
+ }
+ }
+
+ return nil
+}
+
+// Get runs "go get" with the supplied arguments.
+func (c *Client) Get(args ...string) error {
+ if err := c.runGo(context.Background(), os.Stdout, append([]string{"get"}, args...)...); err != nil {
+ errors.Wrapf(err, "failed to get %q", args)
+ }
+ return nil
+}
+
+// Init initializes this as a Go Module with the given path.
+// If path is empty, Go will try to guess.
+// If this succeeds, this project will be marked as Go Module.
+func (c *Client) Init(path string) error {
+ err := c.runGo(context.Background(), os.Stdout, "mod", "init", path)
+ if err != nil {
+ return errors.Wrap(err, "failed to init modules")
+ }
+
+ c.GoModulesFilename = filepath.Join(c.workingDir, goModFilename)
+
+ return nil
+}
+
+func (c *Client) isProbablyModule(path string) bool {
+ return module.CheckPath(path) == nil
+}
+
+func (c *Client) listGoMods() (goModules, error) {
+ if c.GoModulesFilename == "" {
+ return nil, nil
+ }
+
+ out := ioutil.Discard
+ err := c.runGo(context.Background(), out, "mod", "download")
+ if err != nil {
+ return nil, errors.Wrap(err, "failed to download modules")
+ }
+
+ b := &bytes.Buffer{}
+ err = c.runGo(context.Background(), b, "list", "-m", "-json", "all")
+ if err != nil {
+ return nil, errors.Wrap(err, "failed to list modules")
+ }
+
+ var modules goModules
+
+ dec := json.NewDecoder(b)
+ for {
+ m := &goModule{}
+ if err := dec.Decode(m); err != nil {
+ if err == io.EOF {
+ break
+ }
+ return nil, errors.Wrap(err, "failed to decode modules list")
+ }
+
+ modules = append(modules, m)
+ }
+
+ return modules, err
+
+}
+
+func (c *Client) rewriteGoMod(name string, isGoMod map[string]bool) error {
+ data, err := c.rewriteGoModRewrite(name, isGoMod)
+ if err != nil {
+ return err
+ }
+ if data != nil {
+ if err := afero.WriteFile(c.fs, filepath.Join(c.workingDir, name), data, 0666); err != nil {
+ return err
+ }
+ }
+
+ return nil
+}
+
+func (c *Client) rewriteGoModRewrite(name string, isGoMod map[string]bool) ([]byte, error) {
+ if name == goModFilename && c.GoModulesFilename == "" {
+ // Already checked.
+ return nil, nil
+ }
+
+ modlineSplitter := getModlineSplitter(name == goModFilename)
+
+ b := &bytes.Buffer{}
+ f, err := c.fs.Open(filepath.Join(c.workingDir, name))
+ if err != nil {
+ if os.IsNotExist(err) {
+ // It's been deleted.
+ return nil, nil
+ }
+ return nil, err
+ }
+ defer f.Close()
+
+ scanner := bufio.NewScanner(f)
+ var dirty bool
+
+ for scanner.Scan() {
+ line := scanner.Text()
+ var doWrite bool
+
+ if parts := modlineSplitter(line); parts != nil {
+ modname, modver := parts[0], parts[1]
+ modver = strings.TrimSuffix(modver, "/"+goModFilename)
+ modnameVer := modname + " " + modver
+ doWrite = isGoMod[modnameVer]
+ } else {
+ doWrite = true
+ }
+
+ if doWrite {
+ fmt.Fprintln(b, line)
+ } else {
+ dirty = true
+ }
+ }
+
+ if !dirty {
+ // Nothing changed
+ return nil, nil
+ }
+
+ return b.Bytes(), nil
+
+}
+
+func (c *Client) rmVendorDir(vendorDir string) error {
+ modulestxt := filepath.Join(vendorDir, vendorModulesFilename)
+
+ if _, err := c.fs.Stat(vendorDir); err != nil {
+ return nil
+ }
+
+ _, err := c.fs.Stat(modulestxt)
+ if err != nil {
+ // If we have a _vendor dir without modules.txt it sounds like
+ // a _vendor dir created by others.
+ return errors.New("found _vendor dir without modules.txt, skip delete")
+ }
+
+ return c.fs.RemoveAll(vendorDir)
+}
+
+func (c *Client) runGo(
+ ctx context.Context,
+ stdout io.Writer,
+ args ...string) error {
+
+ if c.goBinaryStatus != 0 {
+ return nil
+ }
+
+ stderr := new(bytes.Buffer)
+ cmd := exec.CommandContext(ctx, "go", args...)
+
+ cmd.Env = c.environ
+ cmd.Dir = c.workingDir
+ cmd.Stdout = stdout
+ cmd.Stderr = io.MultiWriter(stderr, os.Stderr)
+
+ if err := cmd.Run(); err != nil {
+ if ee, ok := err.(*exec.Error); ok && ee.Err == exec.ErrNotFound {
+ c.goBinaryStatus = goBinaryStatusNotFound
+ return nil
+ }
+
+ _, ok := err.(*exec.ExitError)
+ if !ok {
+ return errors.Errorf("failed to execute 'go %v': %s %T", args, err, err)
+ }
+
+ // Too old Go version
+ if strings.Contains(stderr.String(), "flag provided but not defined") {
+ c.goBinaryStatus = goBinaryStatusTooOld
+ return nil
+ }
+
+ return errors.Errorf("go command failed: %s", stderr)
+
+ }
+
+ return nil
+}
+
+func (c *Client) tidy(mods Modules, goModOnly bool) error {
+ isGoMod := make(map[string]bool)
+ for _, m := range mods {
+ if m.Owner() == nil {
+ continue
+ }
+ if m.IsGoMod() {
+ // Matching the format in go.mod
+ pathVer := m.Path() + " " + m.Version()
+ isGoMod[pathVer] = true
+ }
+ }
+
+ if err := c.rewriteGoMod(goModFilename, isGoMod); err != nil {
+ return err
+ }
+
+ if goModOnly {
+ return nil
+ }
+
+ if err := c.rewriteGoMod(goSumFilename, isGoMod); err != nil {
+ return err
+ }
+
+ return nil
+}
+
+// ClientConfig configures the module Client.
+type ClientConfig struct {
+ Fs afero.Fs
+ Logger *loggers.Logger
+ IgnoreVendor bool
+ WorkingDir string
+ ThemesDir string // Absolute directory path
+ CacheDir string // Module cache
+ ModuleConfig Config
+}
+
+type goBinaryStatus int
+
+type goModule struct {
+ Path string // module path
+ Version string // module version
+ Versions []string // available module versions (with -versions)
+ Replace *goModule // replaced by this module
+ Time *time.Time // time version was created
+ Update *goModule // available update, if any (with -u)
+ Main bool // is this the main module?
+ Indirect bool // is this module only an indirect dependency of main module?
+ Dir string // directory holding files for this module, if any
+ GoMod string // path to go.mod file for this module, if any
+ Error *goModuleError // error loading module
+}
+
+type goModuleError struct {
+ Err string // the error itself
+}
+
+type goModules []*goModule
+
+func (modules goModules) GetByPath(p string) *goModule {
+ if modules == nil {
+ return nil
+ }
+
+ for _, m := range modules {
+ if strings.EqualFold(p, m.Path) {
+ return m
+ }
+ }
+
+ return nil
+}
+
+func (modules goModules) GetMain() *goModule {
+ for _, m := range modules {
+ if m.Main {
+ return m
+ }
+ }
+
+ return nil
+}
+
+func getModlineSplitter(isGoMod bool) func(line string) []string {
+ if isGoMod {
+ return func(line string) []string {
+ if strings.HasPrefix(line, "require (") {
+ return nil
+ }
+ if !strings.HasPrefix(line, "require") && !strings.HasPrefix(line, "\t") {
+ return nil
+ }
+ line = strings.TrimPrefix(line, "require")
+ line = strings.TrimSpace(line)
+ line = strings.TrimSuffix(line, "// indirect")
+
+ return strings.Fields(line)
+ }
+ }
+
+ return func(line string) []string {
+ return strings.Fields(line)
+ }
+}
+
+func pathVersion(m Module) string {
+ versionStr := m.Version()
+ if m.Vendor() {
+ versionStr += "+vendor"
+ }
+ if versionStr == "" {
+ return m.Path()
+ }
+ return fmt.Sprintf("%s@%s", m.Path(), versionStr)
+}
diff --git a/modules/client_test.go b/modules/client_test.go
new file mode 100644
index 000000000..d8301514d
--- /dev/null
+++ b/modules/client_test.go
@@ -0,0 +1,117 @@
+// 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 (
+ "bytes"
+ "testing"
+
+ "github.com/gohugoio/hugo/common/hugo"
+
+ "github.com/gohugoio/hugo/htesting"
+
+ "github.com/gohugoio/hugo/hugofs"
+
+ "github.com/stretchr/testify/require"
+)
+
+func TestClient(t *testing.T) {
+ if hugo.GoMinorVersion() < 12 {
+ // https://github.com/golang/go/issues/26794
+ // There were some concurrent issues with Go modules in < Go 12.
+ t.Skip("skip this for Go <= 1.11 due to a bug in Go's stdlib")
+ }
+
+ t.Parallel()
+
+ modName := "hugo-modules-basic-test"
+ modPath := "github.com/gohugoio/tests/" + modName
+ modConfig := DefaultModuleConfig
+ modConfig.Imports = []Import{Import{Path: "github.com/gohugoio/hugoTestModules1_darwin/modh2_2"}}
+
+ assert := require.New(t)
+
+ workingDir, clean, err := htesting.CreateTempDir(hugofs.Os, modName)
+ assert.NoError(err)
+ defer clean()
+
+ client := NewClient(ClientConfig{
+ Fs: hugofs.Os,
+ WorkingDir: workingDir,
+ ModuleConfig: modConfig,
+ })
+
+ // Test Init
+ assert.NoError(client.Init(modPath))
+
+ // Test Collect
+ mc, err := client.Collect()
+ assert.NoError(err)
+ assert.Equal(4, len(mc.AllModules))
+ for _, m := range mc.AllModules {
+ assert.NotNil(m)
+ }
+
+ // Test Graph
+ var graphb bytes.Buffer
+ assert.NoError(client.Graph(&graphb))
+
+ expect := `github.com/gohugoio/tests/hugo-modules-basic-test github.com/gohugoio/hugoTestModules1_darwin/modh2_2@v1.4.0
+github.com/gohugoio/hugoTestModules1_darwin/modh2_2@v1.4.0 github.com/gohugoio/hugoTestModules1_darwin/modh2_2_1v@v1.3.0
+github.com/gohugoio/hugoTestModules1_darwin/modh2_2@v1.4.0 github.com/gohugoio/hugoTestModules1_darwin/modh2_2_2@v1.3.0
+`
+
+ assert.Equal(expect, graphb.String())
+
+ // Test Vendor
+ assert.NoError(client.Vendor())
+ graphb.Reset()
+ assert.NoError(client.Graph(&graphb))
+ expectVendored := `github.com/gohugoio/tests/hugo-modules-basic-test github.com/gohugoio/hugoTestModules1_darwin/modh2_2@v1.4.0+vendor
+github.com/gohugoio/tests/hugo-modules-basic-test github.com/gohugoio/hugoTestModules1_darwin/modh2_2_1v@v1.3.0+vendor
+github.com/gohugoio/tests/hugo-modules-basic-test github.com/gohugoio/hugoTestModules1_darwin/modh2_2_2@v1.3.0+vendor
+`
+ assert.Equal(expectVendored, graphb.String())
+
+ // Test the ignoreVendor setting
+ clientIgnoreVendor := NewClient(ClientConfig{
+ Fs: hugofs.Os,
+ WorkingDir: workingDir,
+ ModuleConfig: modConfig,
+ IgnoreVendor: true,
+ })
+
+ graphb.Reset()
+ assert.NoError(clientIgnoreVendor.Graph(&graphb))
+ assert.Equal(expect, graphb.String())
+
+ // Test Tidy
+ assert.NoError(client.Tidy())
+
+}
+
+func TestGetModlineSplitter(t *testing.T) {
+
+ assert := require.New(t)
+
+ gomodSplitter := getModlineSplitter(true)
+
+ assert.Equal([]string{"github.com/BurntSushi/toml", "v0.3.1"}, gomodSplitter("\tgithub.com/BurntSushi/toml v0.3.1"))
+ assert.Equal([]string{"github.com/cpuguy83/go-md2man", "v1.0.8"}, gomodSplitter("\tgithub.com/cpuguy83/go-md2man v1.0.8 // indirect"))
+ assert.Nil(gomodSplitter("require ("))
+
+ gosumSplitter := getModlineSplitter(false)
+ assert.Equal([]string{"github.com/BurntSushi/toml", "v0.3.1"}, gosumSplitter("github.com/BurntSushi/toml v0.3.1"))
+
+}
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(