github.com/gohugoio/hugo@v0.88.1/modules/collect.go (about)

     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 modules
    15  
    16  import (
    17  	"bufio"
    18  	"fmt"
    19  	"os"
    20  	"path/filepath"
    21  	"regexp"
    22  	"strings"
    23  	"time"
    24  
    25  	"github.com/bep/debounce"
    26  	"github.com/gohugoio/hugo/common/loggers"
    27  
    28  	"github.com/spf13/cast"
    29  
    30  	"github.com/gohugoio/hugo/common/maps"
    31  
    32  	"github.com/gohugoio/hugo/common/hugo"
    33  	"github.com/gohugoio/hugo/parser/metadecoders"
    34  
    35  	"github.com/gohugoio/hugo/hugofs/files"
    36  
    37  	"github.com/rogpeppe/go-internal/module"
    38  
    39  	"github.com/pkg/errors"
    40  
    41  	"github.com/gohugoio/hugo/config"
    42  	"github.com/spf13/afero"
    43  )
    44  
    45  var ErrNotExist = errors.New("module does not exist")
    46  
    47  const vendorModulesFilename = "modules.txt"
    48  
    49  // IsNotExist returns whether an error means that a module could not be found.
    50  func IsNotExist(err error) bool {
    51  	return errors.Cause(err) == ErrNotExist
    52  }
    53  
    54  // CreateProjectModule creates modules from the given config.
    55  // This is used in tests only.
    56  func CreateProjectModule(cfg config.Provider) (Module, error) {
    57  	workingDir := cfg.GetString("workingDir")
    58  	var modConfig Config
    59  
    60  	mod := createProjectModule(nil, workingDir, modConfig)
    61  	if err := ApplyProjectConfigDefaults(cfg, mod); err != nil {
    62  		return nil, err
    63  	}
    64  
    65  	return mod, nil
    66  }
    67  
    68  func (h *Client) Collect() (ModulesConfig, error) {
    69  	mc, coll := h.collect(true)
    70  	if coll.err != nil {
    71  		return mc, coll.err
    72  	}
    73  
    74  	if err := (&mc).setActiveMods(h.logger); err != nil {
    75  		return mc, err
    76  	}
    77  
    78  	if h.ccfg.HookBeforeFinalize != nil {
    79  		if err := h.ccfg.HookBeforeFinalize(&mc); err != nil {
    80  			return mc, err
    81  		}
    82  	}
    83  
    84  	if err := (&mc).finalize(h.logger); err != nil {
    85  		return mc, err
    86  	}
    87  
    88  	return mc, nil
    89  }
    90  
    91  func (h *Client) collect(tidy bool) (ModulesConfig, *collector) {
    92  	c := &collector{
    93  		Client: h,
    94  	}
    95  
    96  	c.collect()
    97  	if c.err != nil {
    98  		return ModulesConfig{}, c
    99  	}
   100  
   101  	// https://github.com/gohugoio/hugo/issues/6115
   102  	/*if !c.skipTidy && tidy {
   103  		if err := h.tidy(c.modules, true); err != nil {
   104  			c.err = err
   105  			return ModulesConfig{}, c
   106  		}
   107  	}*/
   108  
   109  	return ModulesConfig{
   110  		AllModules:        c.modules,
   111  		GoModulesFilename: c.GoModulesFilename,
   112  	}, c
   113  }
   114  
   115  type ModulesConfig struct {
   116  	// All modules, including any disabled.
   117  	AllModules Modules
   118  
   119  	// All active modules.
   120  	ActiveModules Modules
   121  
   122  	// Set if this is a Go modules enabled project.
   123  	GoModulesFilename string
   124  }
   125  
   126  func (m *ModulesConfig) setActiveMods(logger loggers.Logger) error {
   127  	var activeMods Modules
   128  	for _, mod := range m.AllModules {
   129  		if !mod.Config().HugoVersion.IsValid() {
   130  			logger.Warnf(`Module %q is not compatible with this Hugo version; run "hugo mod graph" for more information.`, mod.Path())
   131  		}
   132  		if !mod.Disabled() {
   133  			activeMods = append(activeMods, mod)
   134  		}
   135  	}
   136  
   137  	m.ActiveModules = activeMods
   138  
   139  	return nil
   140  }
   141  
   142  func (m *ModulesConfig) finalize(logger loggers.Logger) error {
   143  	for _, mod := range m.AllModules {
   144  		m := mod.(*moduleAdapter)
   145  		m.mounts = filterUnwantedMounts(m.mounts)
   146  	}
   147  	return nil
   148  }
   149  
   150  func filterUnwantedMounts(mounts []Mount) []Mount {
   151  	// Remove duplicates
   152  	seen := make(map[Mount]bool)
   153  	tmp := mounts[:0]
   154  	for _, m := range mounts {
   155  		if !seen[m] {
   156  			tmp = append(tmp, m)
   157  		}
   158  		seen[m] = true
   159  	}
   160  	return tmp
   161  }
   162  
   163  type collected struct {
   164  	// Pick the first and prevent circular loops.
   165  	seen map[string]bool
   166  
   167  	// Maps module path to a _vendor dir. These values are fetched from
   168  	// _vendor/modules.txt, and the first (top-most) will win.
   169  	vendored map[string]vendoredModule
   170  
   171  	// Set if a Go modules enabled project.
   172  	gomods goModules
   173  
   174  	// Ordered list of collected modules, including Go Modules and theme
   175  	// components stored below /themes.
   176  	modules Modules
   177  }
   178  
   179  // Collects and creates a module tree.
   180  type collector struct {
   181  	*Client
   182  
   183  	// Store away any non-fatal error and return at the end.
   184  	err error
   185  
   186  	// Set to disable any Tidy operation in the end.
   187  	skipTidy bool
   188  
   189  	*collected
   190  }
   191  
   192  func (c *collector) initModules() error {
   193  	c.collected = &collected{
   194  		seen:     make(map[string]bool),
   195  		vendored: make(map[string]vendoredModule),
   196  		gomods:   goModules{},
   197  	}
   198  
   199  	// If both these are true, we don't even need Go installed to build.
   200  	if c.ccfg.IgnoreVendor == nil && c.isVendored(c.ccfg.WorkingDir) {
   201  		return nil
   202  	}
   203  
   204  	// We may fail later if we don't find the mods.
   205  	return c.loadModules()
   206  }
   207  
   208  func (c *collector) isSeen(path string) bool {
   209  	key := pathKey(path)
   210  	if c.seen[key] {
   211  		return true
   212  	}
   213  	c.seen[key] = true
   214  	return false
   215  }
   216  
   217  func (c *collector) getVendoredDir(path string) (vendoredModule, bool) {
   218  	v, found := c.vendored[path]
   219  	return v, found
   220  }
   221  
   222  func (c *collector) add(owner *moduleAdapter, moduleImport Import, disabled bool) (*moduleAdapter, error) {
   223  	var (
   224  		mod       *goModule
   225  		moduleDir string
   226  		version   string
   227  		vendored  bool
   228  	)
   229  
   230  	modulePath := moduleImport.Path
   231  	var realOwner Module = owner
   232  
   233  	if !c.ccfg.shouldIgnoreVendor(modulePath) {
   234  		if err := c.collectModulesTXT(owner); err != nil {
   235  			return nil, err
   236  		}
   237  
   238  		// Try _vendor first.
   239  		var vm vendoredModule
   240  		vm, vendored = c.getVendoredDir(modulePath)
   241  		if vendored {
   242  			moduleDir = vm.Dir
   243  			realOwner = vm.Owner
   244  			version = vm.Version
   245  
   246  			if owner.projectMod {
   247  				// We want to keep the go.mod intact with the versions and all.
   248  				c.skipTidy = true
   249  			}
   250  
   251  		}
   252  	}
   253  
   254  	if moduleDir == "" {
   255  		var versionQuery string
   256  		mod = c.gomods.GetByPath(modulePath)
   257  		if mod != nil {
   258  			moduleDir = mod.Dir
   259  			versionQuery = mod.Version
   260  		}
   261  
   262  		if moduleDir == "" {
   263  			if c.GoModulesFilename != "" && isProbablyModule(modulePath) {
   264  				// Try to "go get" it and reload the module configuration.
   265  				if versionQuery == "" {
   266  					// See https://golang.org/ref/mod#version-queries
   267  					// This will select the latest release-version (not beta etc.).
   268  					versionQuery = "upgrade"
   269  				}
   270  				if err := c.Get(fmt.Sprintf("%s@%s", modulePath, versionQuery)); err != nil {
   271  					return nil, err
   272  				}
   273  				if err := c.loadModules(); err != nil {
   274  					return nil, err
   275  				}
   276  
   277  				mod = c.gomods.GetByPath(modulePath)
   278  				if mod != nil {
   279  					moduleDir = mod.Dir
   280  				}
   281  			}
   282  
   283  			// Fall back to project/themes/<mymodule>
   284  			if moduleDir == "" {
   285  				var err error
   286  				moduleDir, err = c.createThemeDirname(modulePath, owner.projectMod || moduleImport.pathProjectReplaced)
   287  				if err != nil {
   288  					c.err = err
   289  					return nil, nil
   290  				}
   291  				if found, _ := afero.Exists(c.fs, moduleDir); !found {
   292  					c.err = c.wrapModuleNotFound(errors.Errorf(`module %q not found; either add it as a Hugo Module or store it in %q.`, modulePath, c.ccfg.ThemesDir))
   293  					return nil, nil
   294  				}
   295  			}
   296  		}
   297  	}
   298  
   299  	if found, _ := afero.Exists(c.fs, moduleDir); !found {
   300  		c.err = c.wrapModuleNotFound(errors.Errorf("%q not found", moduleDir))
   301  		return nil, nil
   302  	}
   303  
   304  	if !strings.HasSuffix(moduleDir, fileSeparator) {
   305  		moduleDir += fileSeparator
   306  	}
   307  
   308  	ma := &moduleAdapter{
   309  		dir:      moduleDir,
   310  		vendor:   vendored,
   311  		disabled: disabled,
   312  		gomod:    mod,
   313  		version:  version,
   314  		// This may be the owner of the _vendor dir
   315  		owner: realOwner,
   316  	}
   317  
   318  	if mod == nil {
   319  		ma.path = modulePath
   320  	}
   321  
   322  	if !moduleImport.IgnoreConfig {
   323  		if err := c.applyThemeConfig(ma); err != nil {
   324  			return nil, err
   325  		}
   326  	}
   327  
   328  	if err := c.applyMounts(moduleImport, ma); err != nil {
   329  		return nil, err
   330  	}
   331  
   332  	c.modules = append(c.modules, ma)
   333  	return ma, nil
   334  }
   335  
   336  func (c *collector) addAndRecurse(owner *moduleAdapter, disabled bool) error {
   337  	moduleConfig := owner.Config()
   338  	if owner.projectMod {
   339  		if err := c.applyMounts(Import{}, owner); err != nil {
   340  			return err
   341  		}
   342  	}
   343  
   344  	for _, moduleImport := range moduleConfig.Imports {
   345  		disabled := disabled || moduleImport.Disable
   346  
   347  		if !c.isSeen(moduleImport.Path) {
   348  			tc, err := c.add(owner, moduleImport, disabled)
   349  			if err != nil {
   350  				return err
   351  			}
   352  			if tc == nil || moduleImport.IgnoreImports {
   353  				continue
   354  			}
   355  			if err := c.addAndRecurse(tc, disabled); err != nil {
   356  				return err
   357  			}
   358  		}
   359  	}
   360  	return nil
   361  }
   362  
   363  func (c *collector) applyMounts(moduleImport Import, mod *moduleAdapter) error {
   364  	if moduleImport.NoMounts {
   365  		mod.mounts = nil
   366  		return nil
   367  	}
   368  
   369  	mounts := moduleImport.Mounts
   370  
   371  	modConfig := mod.Config()
   372  
   373  	if len(mounts) == 0 {
   374  		// Mounts not defined by the import.
   375  		mounts = modConfig.Mounts
   376  	}
   377  
   378  	if !mod.projectMod && len(mounts) == 0 {
   379  		// Create default mount points for every component folder that
   380  		// exists in the module.
   381  		for _, componentFolder := range files.ComponentFolders {
   382  			sourceDir := filepath.Join(mod.Dir(), componentFolder)
   383  			_, err := c.fs.Stat(sourceDir)
   384  			if err == nil {
   385  				mounts = append(mounts, Mount{
   386  					Source: componentFolder,
   387  					Target: componentFolder,
   388  				})
   389  			}
   390  		}
   391  	}
   392  
   393  	var err error
   394  	mounts, err = c.normalizeMounts(mod, mounts)
   395  	if err != nil {
   396  		return err
   397  	}
   398  
   399  	mounts, err = c.mountCommonJSConfig(mod, mounts)
   400  	if err != nil {
   401  		return err
   402  	}
   403  
   404  	mod.mounts = mounts
   405  	return nil
   406  }
   407  
   408  func (c *collector) applyThemeConfig(tc *moduleAdapter) error {
   409  	var (
   410  		configFilename string
   411  		themeCfg       map[string]interface{}
   412  		hasConfigFile  bool
   413  		err            error
   414  	)
   415  
   416  	// Viper supports more, but this is the sub-set supported by Hugo.
   417  	for _, configFormats := range config.ValidConfigFileExtensions {
   418  		configFilename = filepath.Join(tc.Dir(), "config."+configFormats)
   419  		hasConfigFile, _ = afero.Exists(c.fs, configFilename)
   420  		if hasConfigFile {
   421  			break
   422  		}
   423  	}
   424  
   425  	// The old theme information file.
   426  	themeTOML := filepath.Join(tc.Dir(), "theme.toml")
   427  
   428  	hasThemeTOML, _ := afero.Exists(c.fs, themeTOML)
   429  	if hasThemeTOML {
   430  		data, err := afero.ReadFile(c.fs, themeTOML)
   431  		if err != nil {
   432  			return err
   433  		}
   434  		themeCfg, err = metadecoders.Default.UnmarshalToMap(data, metadecoders.TOML)
   435  		if err != nil {
   436  			c.logger.Warnf("Failed to read module config for %q in %q: %s", tc.Path(), themeTOML, err)
   437  		} else {
   438  			maps.PrepareParams(themeCfg)
   439  		}
   440  	}
   441  
   442  	if hasConfigFile {
   443  		if configFilename != "" {
   444  			var err error
   445  			tc.cfg, err = config.FromFile(c.fs, configFilename)
   446  			if err != nil {
   447  				return err
   448  			}
   449  		}
   450  
   451  		tc.configFilenames = append(tc.configFilenames, configFilename)
   452  
   453  	}
   454  
   455  	// Also check for a config dir, which we overlay on top of the file configuration.
   456  	configDir := filepath.Join(tc.Dir(), "config")
   457  	dcfg, dirnames, err := config.LoadConfigFromDir(c.fs, configDir, c.ccfg.Environment)
   458  	if err != nil {
   459  		return err
   460  	}
   461  
   462  	if len(dirnames) > 0 {
   463  		tc.configFilenames = append(tc.configFilenames, dirnames...)
   464  
   465  		if hasConfigFile {
   466  			// Set will overwrite existing keys.
   467  			tc.cfg.Set("", dcfg.Get(""))
   468  		} else {
   469  			tc.cfg = dcfg
   470  		}
   471  	}
   472  
   473  	config, err := decodeConfig(tc.cfg, c.moduleConfig.replacementsMap)
   474  	if err != nil {
   475  		return err
   476  	}
   477  
   478  	const oldVersionKey = "min_version"
   479  
   480  	if hasThemeTOML {
   481  
   482  		// Merge old with new
   483  		if minVersion, found := themeCfg[oldVersionKey]; found {
   484  			if config.HugoVersion.Min == "" {
   485  				config.HugoVersion.Min = hugo.VersionString(cast.ToString(minVersion))
   486  			}
   487  		}
   488  
   489  		if config.Params == nil {
   490  			config.Params = make(map[string]interface{})
   491  		}
   492  
   493  		for k, v := range themeCfg {
   494  			if k == oldVersionKey {
   495  				continue
   496  			}
   497  			config.Params[k] = v
   498  		}
   499  
   500  	}
   501  
   502  	tc.config = config
   503  
   504  	return nil
   505  }
   506  
   507  func (c *collector) collect() {
   508  	defer c.logger.PrintTimerIfDelayed(time.Now(), "hugo: collected modules")
   509  	d := debounce.New(2 * time.Second)
   510  	d(func() {
   511  		c.logger.Println("hugo: downloading modules …")
   512  	})
   513  	defer d(func() {})
   514  
   515  	if err := c.initModules(); err != nil {
   516  		c.err = err
   517  		return
   518  	}
   519  
   520  	projectMod := createProjectModule(c.gomods.GetMain(), c.ccfg.WorkingDir, c.moduleConfig)
   521  
   522  	if err := c.addAndRecurse(projectMod, false); err != nil {
   523  		c.err = err
   524  		return
   525  	}
   526  
   527  	// Add the project mod on top.
   528  	c.modules = append(Modules{projectMod}, c.modules...)
   529  }
   530  
   531  func (c *collector) isVendored(dir string) bool {
   532  	_, err := c.fs.Stat(filepath.Join(dir, vendord, vendorModulesFilename))
   533  	return err == nil
   534  }
   535  
   536  func (c *collector) collectModulesTXT(owner Module) error {
   537  	vendorDir := filepath.Join(owner.Dir(), vendord)
   538  	filename := filepath.Join(vendorDir, vendorModulesFilename)
   539  
   540  	f, err := c.fs.Open(filename)
   541  	if err != nil {
   542  		if os.IsNotExist(err) {
   543  			return nil
   544  		}
   545  
   546  		return err
   547  	}
   548  
   549  	defer f.Close()
   550  
   551  	scanner := bufio.NewScanner(f)
   552  
   553  	for scanner.Scan() {
   554  		// # github.com/alecthomas/chroma v0.6.3
   555  		line := scanner.Text()
   556  		line = strings.Trim(line, "# ")
   557  		line = strings.TrimSpace(line)
   558  		parts := strings.Fields(line)
   559  		if len(parts) != 2 {
   560  			return errors.Errorf("invalid modules list: %q", filename)
   561  		}
   562  		path := parts[0]
   563  
   564  		shouldAdd := c.Client.moduleConfig.VendorClosest
   565  
   566  		if !shouldAdd {
   567  			if _, found := c.vendored[path]; !found {
   568  				shouldAdd = true
   569  			}
   570  		}
   571  
   572  		if shouldAdd {
   573  			c.vendored[path] = vendoredModule{
   574  				Owner:   owner,
   575  				Dir:     filepath.Join(vendorDir, path),
   576  				Version: parts[1],
   577  			}
   578  		}
   579  
   580  	}
   581  	return nil
   582  }
   583  
   584  func (c *collector) loadModules() error {
   585  	modules, err := c.listGoMods()
   586  	if err != nil {
   587  		return err
   588  	}
   589  	c.gomods = modules
   590  	return nil
   591  }
   592  
   593  // Matches postcss.config.js etc.
   594  var commonJSConfigs = regexp.MustCompile(`(babel|postcss|tailwind)\.config\.js`)
   595  
   596  func (c *collector) mountCommonJSConfig(owner *moduleAdapter, mounts []Mount) ([]Mount, error) {
   597  	for _, m := range mounts {
   598  		if strings.HasPrefix(m.Target, files.JsConfigFolderMountPrefix) {
   599  			// This follows the convention of the other component types (assets, content, etc.),
   600  			// if one or more is specified by the user, we skip the defaults.
   601  			// These mounts were added to Hugo in 0.75.
   602  			return mounts, nil
   603  		}
   604  	}
   605  
   606  	// Mount the common JS config files.
   607  	fis, err := afero.ReadDir(c.fs, owner.Dir())
   608  	if err != nil {
   609  		return mounts, err
   610  	}
   611  
   612  	for _, fi := range fis {
   613  		n := fi.Name()
   614  
   615  		should := n == files.FilenamePackageHugoJSON || n == files.FilenamePackageJSON
   616  		should = should || commonJSConfigs.MatchString(n)
   617  
   618  		if should {
   619  			mounts = append(mounts, Mount{
   620  				Source: n,
   621  				Target: filepath.Join(files.ComponentFolderAssets, files.FolderJSConfig, n),
   622  			})
   623  		}
   624  
   625  	}
   626  
   627  	return mounts, nil
   628  }
   629  
   630  func (c *collector) normalizeMounts(owner *moduleAdapter, mounts []Mount) ([]Mount, error) {
   631  	var out []Mount
   632  	dir := owner.Dir()
   633  
   634  	for _, mnt := range mounts {
   635  		errMsg := fmt.Sprintf("invalid module config for %q", owner.Path())
   636  
   637  		if mnt.Source == "" || mnt.Target == "" {
   638  			return nil, errors.New(errMsg + ": both source and target must be set")
   639  		}
   640  
   641  		mnt.Source = filepath.Clean(mnt.Source)
   642  		mnt.Target = filepath.Clean(mnt.Target)
   643  		var sourceDir string
   644  
   645  		if owner.projectMod && filepath.IsAbs(mnt.Source) {
   646  			// Abs paths in the main project is allowed.
   647  			sourceDir = mnt.Source
   648  		} else {
   649  			sourceDir = filepath.Join(dir, mnt.Source)
   650  		}
   651  
   652  		// Verify that Source exists
   653  		_, err := c.fs.Stat(sourceDir)
   654  		if err != nil {
   655  			continue
   656  		}
   657  
   658  		// Verify that target points to one of the predefined component dirs
   659  		targetBase := mnt.Target
   660  		idxPathSep := strings.Index(mnt.Target, string(os.PathSeparator))
   661  		if idxPathSep != -1 {
   662  			targetBase = mnt.Target[0:idxPathSep]
   663  		}
   664  		if !files.IsComponentFolder(targetBase) {
   665  			return nil, errors.Errorf("%s: mount target must be one of: %v", errMsg, files.ComponentFolders)
   666  		}
   667  
   668  		out = append(out, mnt)
   669  	}
   670  
   671  	return out, nil
   672  }
   673  
   674  func (c *collector) wrapModuleNotFound(err error) error {
   675  	err = errors.Wrap(ErrNotExist, err.Error())
   676  	if c.GoModulesFilename == "" {
   677  		return err
   678  	}
   679  
   680  	baseMsg := "we found a go.mod file in your project, but"
   681  
   682  	switch c.goBinaryStatus {
   683  	case goBinaryStatusNotFound:
   684  		return errors.Wrap(err, baseMsg+" you need to install Go to use it. See https://golang.org/dl/.")
   685  	case goBinaryStatusTooOld:
   686  		return errors.Wrap(err, baseMsg+" you need to a newer version of Go to use it. See https://golang.org/dl/.")
   687  	}
   688  
   689  	return err
   690  }
   691  
   692  type vendoredModule struct {
   693  	Owner   Module
   694  	Dir     string
   695  	Version string
   696  }
   697  
   698  func createProjectModule(gomod *goModule, workingDir string, conf Config) *moduleAdapter {
   699  	// Create a pseudo module for the main project.
   700  	var path string
   701  	if gomod == nil {
   702  		path = "project"
   703  	}
   704  
   705  	return &moduleAdapter{
   706  		path:       path,
   707  		dir:        workingDir,
   708  		gomod:      gomod,
   709  		projectMod: true,
   710  		config:     conf,
   711  	}
   712  }
   713  
   714  // In the first iteration of Hugo Modules, we do not support multiple
   715  // major versions running at the same time, so we pick the first (upper most).
   716  // We will investigate namespaces in future versions.
   717  // TODO(bep) add a warning when the above happens.
   718  func pathKey(p string) string {
   719  	prefix, _, _ := module.SplitPathVersion(p)
   720  
   721  	return strings.ToLower(prefix)
   722  }