github.com/linchen2chris/hugo@v0.0.0-20230307053224-cec209389705/modules/client.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  	"bytes"
    19  	"context"
    20  	"encoding/json"
    21  	"fmt"
    22  	"io"
    23  	"os"
    24  	"os/exec"
    25  	"path/filepath"
    26  	"regexp"
    27  	"strings"
    28  	"time"
    29  
    30  	"github.com/gohugoio/hugo/common/collections"
    31  	"github.com/gohugoio/hugo/common/herrors"
    32  	"github.com/gohugoio/hugo/common/hexec"
    33  
    34  	hglob "github.com/gohugoio/hugo/hugofs/glob"
    35  
    36  	"github.com/gobwas/glob"
    37  
    38  	"github.com/gohugoio/hugo/hugofs"
    39  
    40  	"github.com/gohugoio/hugo/hugofs/files"
    41  
    42  	"github.com/gohugoio/hugo/common/loggers"
    43  
    44  	"github.com/gohugoio/hugo/config"
    45  
    46  	"github.com/rogpeppe/go-internal/module"
    47  
    48  	"github.com/gohugoio/hugo/common/hugio"
    49  
    50  	"errors"
    51  
    52  	"github.com/spf13/afero"
    53  )
    54  
    55  var fileSeparator = string(os.PathSeparator)
    56  
    57  const (
    58  	goBinaryStatusOK goBinaryStatus = iota
    59  	goBinaryStatusNotFound
    60  	goBinaryStatusTooOld
    61  )
    62  
    63  // The "vendor" dir is reserved for Go Modules.
    64  const vendord = "_vendor"
    65  
    66  const (
    67  	goModFilename = "go.mod"
    68  	goSumFilename = "go.sum"
    69  )
    70  
    71  // NewClient creates a new Client that can be used to manage the Hugo Components
    72  // in a given workingDir.
    73  // The Client will resolve the dependencies recursively, but needs the top
    74  // level imports to start out.
    75  func NewClient(cfg ClientConfig) *Client {
    76  	fs := cfg.Fs
    77  	n := filepath.Join(cfg.WorkingDir, goModFilename)
    78  	goModEnabled, _ := afero.Exists(fs, n)
    79  	var goModFilename string
    80  	if goModEnabled {
    81  		goModFilename = n
    82  	}
    83  
    84  	var env []string
    85  	mcfg := cfg.ModuleConfig
    86  
    87  	config.SetEnvVars(&env,
    88  		"PWD", cfg.WorkingDir,
    89  		"GO111MODULE", "on",
    90  		"GOPROXY", mcfg.Proxy,
    91  		"GOPRIVATE", mcfg.Private,
    92  		"GONOPROXY", mcfg.NoProxy,
    93  		"GOPATH", cfg.CacheDir,
    94  		"GOWORK", mcfg.Workspace, // Requires Go 1.18, see https://tip.golang.org/doc/go1.18
    95  		// GOCACHE was introduced in Go 1.15. This matches the location derived from GOPATH above.
    96  		"GOCACHE", filepath.Join(cfg.CacheDir, "pkg", "mod"),
    97  	)
    98  
    99  	logger := cfg.Logger
   100  	if logger == nil {
   101  		logger = loggers.NewWarningLogger()
   102  	}
   103  
   104  	var noVendor glob.Glob
   105  	if cfg.ModuleConfig.NoVendor != "" {
   106  		noVendor, _ = hglob.GetGlob(hglob.NormalizePath(cfg.ModuleConfig.NoVendor))
   107  	}
   108  
   109  	return &Client{
   110  		fs:                fs,
   111  		ccfg:              cfg,
   112  		logger:            logger,
   113  		noVendor:          noVendor,
   114  		moduleConfig:      mcfg,
   115  		environ:           env,
   116  		GoModulesFilename: goModFilename,
   117  	}
   118  }
   119  
   120  // Client contains most of the API provided by this package.
   121  type Client struct {
   122  	fs     afero.Fs
   123  	logger loggers.Logger
   124  
   125  	noVendor glob.Glob
   126  
   127  	ccfg ClientConfig
   128  
   129  	// The top level module config
   130  	moduleConfig Config
   131  
   132  	// Environment variables used in "go get" etc.
   133  	environ []string
   134  
   135  	// Set when Go modules are initialized in the current repo, that is:
   136  	// a go.mod file exists.
   137  	GoModulesFilename string
   138  
   139  	// Set if we get a exec.ErrNotFound when running Go, which is most likely
   140  	// due to being run on a system without Go installed. We record it here
   141  	// so we can give an instructional error at the end if module/theme
   142  	// resolution fails.
   143  	goBinaryStatus goBinaryStatus
   144  }
   145  
   146  // Graph writes a module dependenchy graph to the given writer.
   147  func (c *Client) Graph(w io.Writer) error {
   148  	mc, coll := c.collect(true)
   149  	if coll.err != nil {
   150  		return coll.err
   151  	}
   152  	for _, module := range mc.AllModules {
   153  		if module.Owner() == nil {
   154  			continue
   155  		}
   156  
   157  		prefix := ""
   158  		if module.Disabled() {
   159  			prefix = "DISABLED "
   160  		}
   161  		dep := pathVersion(module.Owner()) + " " + pathVersion(module)
   162  		if replace := module.Replace(); replace != nil {
   163  			if replace.Version() != "" {
   164  				dep += " => " + pathVersion(replace)
   165  			} else {
   166  				// Local dir.
   167  				dep += " => " + replace.Dir()
   168  			}
   169  		}
   170  		fmt.Fprintln(w, prefix+dep)
   171  	}
   172  
   173  	return nil
   174  }
   175  
   176  // Tidy can be used to remove unused dependencies from go.mod and go.sum.
   177  func (c *Client) Tidy() error {
   178  	tc, coll := c.collect(false)
   179  	if coll.err != nil {
   180  		return coll.err
   181  	}
   182  
   183  	if coll.skipTidy {
   184  		return nil
   185  	}
   186  
   187  	return c.tidy(tc.AllModules, false)
   188  }
   189  
   190  // Vendor writes all the module dependencies to a _vendor folder.
   191  //
   192  // Unlike Go, we support it for any level.
   193  //
   194  // We, by default, use the /_vendor folder first, if found. To disable,
   195  // run with
   196  //
   197  //	hugo --ignoreVendorPaths=".*"
   198  //
   199  // Given a module tree, Hugo will pick the first module for a given path,
   200  // meaning that if the top-level module is vendored, that will be the full
   201  // set of dependencies.
   202  func (c *Client) Vendor() error {
   203  	vendorDir := filepath.Join(c.ccfg.WorkingDir, vendord)
   204  	if err := c.rmVendorDir(vendorDir); err != nil {
   205  		return err
   206  	}
   207  	if err := c.fs.MkdirAll(vendorDir, 0755); err != nil {
   208  		return err
   209  	}
   210  
   211  	// Write the modules list to modules.txt.
   212  	//
   213  	// On the form:
   214  	//
   215  	// # github.com/alecthomas/chroma v0.6.3
   216  	//
   217  	// This is how "go mod vendor" does it. Go also lists
   218  	// the packages below it, but that is currently not applicable to us.
   219  	//
   220  	var modulesContent bytes.Buffer
   221  
   222  	tc, coll := c.collect(true)
   223  	if coll.err != nil {
   224  		return coll.err
   225  	}
   226  
   227  	for _, t := range tc.AllModules {
   228  		if t.Owner() == nil {
   229  			// This is the project.
   230  			continue
   231  		}
   232  
   233  		if !c.shouldVendor(t.Path()) {
   234  			continue
   235  		}
   236  
   237  		if !t.IsGoMod() && !t.Vendor() {
   238  			// We currently do not vendor components living in the
   239  			// theme directory, see https://github.com/gohugoio/hugo/issues/5993
   240  			continue
   241  		}
   242  
   243  		// See https://github.com/gohugoio/hugo/issues/8239
   244  		// This is an error situation. We need something to vendor.
   245  		if t.Mounts() == nil {
   246  			return fmt.Errorf("cannot vendor module %q, need at least one mount", t.Path())
   247  		}
   248  
   249  		fmt.Fprintln(&modulesContent, "# "+t.Path()+" "+t.Version())
   250  
   251  		dir := t.Dir()
   252  
   253  		for _, mount := range t.Mounts() {
   254  			sourceFilename := filepath.Join(dir, mount.Source)
   255  			targetFilename := filepath.Join(vendorDir, t.Path(), mount.Source)
   256  			fi, err := c.fs.Stat(sourceFilename)
   257  			if err != nil {
   258  				return fmt.Errorf("failed to vendor module: %w", err)
   259  			}
   260  
   261  			if fi.IsDir() {
   262  				if err := hugio.CopyDir(c.fs, sourceFilename, targetFilename, nil); err != nil {
   263  					return fmt.Errorf("failed to copy module to vendor dir: %w", err)
   264  				}
   265  			} else {
   266  				targetDir := filepath.Dir(targetFilename)
   267  
   268  				if err := c.fs.MkdirAll(targetDir, 0755); err != nil {
   269  					return fmt.Errorf("failed to make target dir: %w", err)
   270  				}
   271  
   272  				if err := hugio.CopyFile(c.fs, sourceFilename, targetFilename); err != nil {
   273  					return fmt.Errorf("failed to copy module file to vendor: %w", err)
   274  				}
   275  			}
   276  		}
   277  
   278  		// Include the resource cache if present.
   279  		resourcesDir := filepath.Join(dir, files.FolderResources)
   280  		_, err := c.fs.Stat(resourcesDir)
   281  		if err == nil {
   282  			if err := hugio.CopyDir(c.fs, resourcesDir, filepath.Join(vendorDir, t.Path(), files.FolderResources), nil); err != nil {
   283  				return fmt.Errorf("failed to copy resources to vendor dir: %w", err)
   284  			}
   285  		}
   286  
   287  		// Include the config directory if present.
   288  		configDir := filepath.Join(dir, "config")
   289  		_, err = c.fs.Stat(configDir)
   290  		if err == nil {
   291  			if err := hugio.CopyDir(c.fs, configDir, filepath.Join(vendorDir, t.Path(), "config"), nil); err != nil {
   292  				return fmt.Errorf("failed to copy config dir to vendor dir: %w", err)
   293  			}
   294  		}
   295  
   296  		// Also include any theme.toml or config.* files in the root.
   297  		configFiles, _ := afero.Glob(c.fs, filepath.Join(dir, "config.*"))
   298  		configFiles = append(configFiles, filepath.Join(dir, "theme.toml"))
   299  		for _, configFile := range configFiles {
   300  			if err := hugio.CopyFile(c.fs, configFile, filepath.Join(vendorDir, t.Path(), filepath.Base(configFile))); err != nil {
   301  				if !herrors.IsNotExist(err) {
   302  					return err
   303  				}
   304  			}
   305  		}
   306  	}
   307  
   308  	if modulesContent.Len() > 0 {
   309  		if err := afero.WriteFile(c.fs, filepath.Join(vendorDir, vendorModulesFilename), modulesContent.Bytes(), 0666); err != nil {
   310  			return err
   311  		}
   312  	}
   313  
   314  	return nil
   315  }
   316  
   317  // Get runs "go get" with the supplied arguments.
   318  func (c *Client) Get(args ...string) error {
   319  	if len(args) == 0 || (len(args) == 1 && strings.Contains(args[0], "-u")) {
   320  		update := len(args) != 0
   321  		patch := update && (args[0] == "-u=patch") //
   322  
   323  		// We need to be explicit about the modules to get.
   324  		for _, m := range c.moduleConfig.Imports {
   325  			if !isProbablyModule(m.Path) {
   326  				// Skip themes/components stored below /themes etc.
   327  				// There may be false positives in the above, but those
   328  				// should be rare, and they will fail below with an
   329  				// "cannot find module providing ..." message.
   330  				continue
   331  			}
   332  			var args []string
   333  
   334  			if update && !patch {
   335  				args = append(args, "-u")
   336  			} else if update && patch {
   337  				args = append(args, "-u=patch")
   338  			}
   339  			args = append(args, m.Path)
   340  
   341  			if err := c.get(args...); err != nil {
   342  				return err
   343  			}
   344  		}
   345  
   346  		return nil
   347  	}
   348  
   349  	return c.get(args...)
   350  }
   351  
   352  func (c *Client) get(args ...string) error {
   353  	var hasD bool
   354  	for _, arg := range args {
   355  		if arg == "-d" {
   356  			hasD = true
   357  			break
   358  		}
   359  	}
   360  	if !hasD {
   361  		// go get without the -d flag does not make sense to us, as
   362  		// it will try to build and install go packages.
   363  		args = append([]string{"-d"}, args...)
   364  	}
   365  	if err := c.runGo(context.Background(), c.logger.Out(), append([]string{"get"}, args...)...); err != nil {
   366  		return fmt.Errorf("failed to get %q: %w", args, err)
   367  	}
   368  	return nil
   369  }
   370  
   371  // Init initializes this as a Go Module with the given path.
   372  // If path is empty, Go will try to guess.
   373  // If this succeeds, this project will be marked as Go Module.
   374  func (c *Client) Init(path string) error {
   375  	err := c.runGo(context.Background(), c.logger.Out(), "mod", "init", path)
   376  	if err != nil {
   377  		return fmt.Errorf("failed to init modules: %w", err)
   378  	}
   379  
   380  	c.GoModulesFilename = filepath.Join(c.ccfg.WorkingDir, goModFilename)
   381  
   382  	return nil
   383  }
   384  
   385  var verifyErrorDirRe = regexp.MustCompile(`dir has been modified \((.*?)\)`)
   386  
   387  // Verify checks that the dependencies of the current module,
   388  // which are stored in a local downloaded source cache, have not been
   389  // modified since being downloaded.
   390  func (c *Client) Verify(clean bool) error {
   391  	// TODO(bep) add path to mod clean
   392  	err := c.runVerify()
   393  	if err != nil {
   394  		if clean {
   395  			m := verifyErrorDirRe.FindAllStringSubmatch(err.Error(), -1)
   396  			if m != nil {
   397  				for i := 0; i < len(m); i++ {
   398  					c, err := hugofs.MakeReadableAndRemoveAllModulePkgDir(c.fs, m[i][1])
   399  					if err != nil {
   400  						return err
   401  					}
   402  					fmt.Println("Cleaned", c)
   403  				}
   404  			}
   405  			// Try to verify it again.
   406  			err = c.runVerify()
   407  		}
   408  	}
   409  	return err
   410  }
   411  
   412  func (c *Client) Clean(pattern string) error {
   413  	mods, err := c.listGoMods()
   414  	if err != nil {
   415  		return err
   416  	}
   417  
   418  	var g glob.Glob
   419  
   420  	if pattern != "" {
   421  		var err error
   422  		g, err = hglob.GetGlob(pattern)
   423  		if err != nil {
   424  			return err
   425  		}
   426  	}
   427  
   428  	for _, m := range mods {
   429  		if m.Replace != nil || m.Main {
   430  			continue
   431  		}
   432  
   433  		if g != nil && !g.Match(m.Path) {
   434  			continue
   435  		}
   436  		_, err = hugofs.MakeReadableAndRemoveAllModulePkgDir(c.fs, m.Dir)
   437  		if err == nil {
   438  			c.logger.Printf("hugo: cleaned module cache for %q", m.Path)
   439  		}
   440  	}
   441  	return err
   442  }
   443  
   444  func (c *Client) runVerify() error {
   445  	return c.runGo(context.Background(), io.Discard, "mod", "verify")
   446  }
   447  
   448  func isProbablyModule(path string) bool {
   449  	return module.CheckPath(path) == nil
   450  }
   451  
   452  func (c *Client) listGoMods() (goModules, error) {
   453  	if c.GoModulesFilename == "" || !c.moduleConfig.hasModuleImport() {
   454  		return nil, nil
   455  	}
   456  
   457  	downloadModules := func(modules ...string) error {
   458  		args := []string{"mod", "download"}
   459  		args = append(args, modules...)
   460  		out := io.Discard
   461  		err := c.runGo(context.Background(), out, args...)
   462  		if err != nil {
   463  			return fmt.Errorf("failed to download modules: %w", err)
   464  		}
   465  		return nil
   466  	}
   467  
   468  	if err := downloadModules(); err != nil {
   469  		return nil, err
   470  	}
   471  
   472  	listAndDecodeModules := func(handle func(m *goModule) error, modules ...string) error {
   473  		b := &bytes.Buffer{}
   474  		args := []string{"list", "-m", "-json"}
   475  		if len(modules) > 0 {
   476  			args = append(args, modules...)
   477  		} else {
   478  			args = append(args, "all")
   479  		}
   480  		err := c.runGo(context.Background(), b, args...)
   481  		if err != nil {
   482  			return fmt.Errorf("failed to list modules: %w", err)
   483  		}
   484  
   485  		dec := json.NewDecoder(b)
   486  		for {
   487  			m := &goModule{}
   488  			if err := dec.Decode(m); err != nil {
   489  				if err == io.EOF {
   490  					break
   491  				}
   492  				return fmt.Errorf("failed to decode modules list: %w", err)
   493  			}
   494  
   495  			if err := handle(m); err != nil {
   496  				return err
   497  			}
   498  		}
   499  		return nil
   500  	}
   501  
   502  	var modules goModules
   503  	err := listAndDecodeModules(func(m *goModule) error {
   504  		modules = append(modules, m)
   505  		return nil
   506  	})
   507  	if err != nil {
   508  		return nil, err
   509  	}
   510  
   511  	// From Go 1.17, go lazy loads transitive dependencies.
   512  	// That does not work for us.
   513  	// So, download these modules and update the Dir in the modules list.
   514  	var modulesToDownload []string
   515  	for _, m := range modules {
   516  		if m.Dir == "" {
   517  			modulesToDownload = append(modulesToDownload, fmt.Sprintf("%s@%s", m.Path, m.Version))
   518  		}
   519  	}
   520  
   521  	if len(modulesToDownload) > 0 {
   522  		if err := downloadModules(modulesToDownload...); err != nil {
   523  			return nil, err
   524  		}
   525  		err := listAndDecodeModules(func(m *goModule) error {
   526  			if mm := modules.GetByPath(m.Path); mm != nil {
   527  				mm.Dir = m.Dir
   528  			}
   529  			return nil
   530  		}, modulesToDownload...)
   531  		if err != nil {
   532  			return nil, err
   533  		}
   534  	}
   535  
   536  	return modules, err
   537  }
   538  
   539  func (c *Client) rewriteGoMod(name string, isGoMod map[string]bool) error {
   540  	data, err := c.rewriteGoModRewrite(name, isGoMod)
   541  	if err != nil {
   542  		return err
   543  	}
   544  	if data != nil {
   545  		if err := afero.WriteFile(c.fs, filepath.Join(c.ccfg.WorkingDir, name), data, 0666); err != nil {
   546  			return err
   547  		}
   548  	}
   549  
   550  	return nil
   551  }
   552  
   553  func (c *Client) rewriteGoModRewrite(name string, isGoMod map[string]bool) ([]byte, error) {
   554  	if name == goModFilename && c.GoModulesFilename == "" {
   555  		// Already checked.
   556  		return nil, nil
   557  	}
   558  
   559  	modlineSplitter := getModlineSplitter(name == goModFilename)
   560  
   561  	b := &bytes.Buffer{}
   562  	f, err := c.fs.Open(filepath.Join(c.ccfg.WorkingDir, name))
   563  	if err != nil {
   564  		if herrors.IsNotExist(err) {
   565  			// It's been deleted.
   566  			return nil, nil
   567  		}
   568  		return nil, err
   569  	}
   570  	defer f.Close()
   571  
   572  	scanner := bufio.NewScanner(f)
   573  	var dirty bool
   574  
   575  	for scanner.Scan() {
   576  		line := scanner.Text()
   577  		var doWrite bool
   578  
   579  		if parts := modlineSplitter(line); parts != nil {
   580  			modname, modver := parts[0], parts[1]
   581  			modver = strings.TrimSuffix(modver, "/"+goModFilename)
   582  			modnameVer := modname + " " + modver
   583  			doWrite = isGoMod[modnameVer]
   584  		} else {
   585  			doWrite = true
   586  		}
   587  
   588  		if doWrite {
   589  			fmt.Fprintln(b, line)
   590  		} else {
   591  			dirty = true
   592  		}
   593  	}
   594  
   595  	if !dirty {
   596  		// Nothing changed
   597  		return nil, nil
   598  	}
   599  
   600  	return b.Bytes(), nil
   601  }
   602  
   603  func (c *Client) rmVendorDir(vendorDir string) error {
   604  	modulestxt := filepath.Join(vendorDir, vendorModulesFilename)
   605  
   606  	if _, err := c.fs.Stat(vendorDir); err != nil {
   607  		return nil
   608  	}
   609  
   610  	_, err := c.fs.Stat(modulestxt)
   611  	if err != nil {
   612  		// If we have a _vendor dir without modules.txt it sounds like
   613  		// a _vendor dir created by others.
   614  		return errors.New("found _vendor dir without modules.txt, skip delete")
   615  	}
   616  
   617  	return c.fs.RemoveAll(vendorDir)
   618  }
   619  
   620  func (c *Client) runGo(
   621  	ctx context.Context,
   622  	stdout io.Writer,
   623  	args ...string) error {
   624  	if c.goBinaryStatus != 0 {
   625  		return nil
   626  	}
   627  
   628  	stderr := new(bytes.Buffer)
   629  
   630  	argsv := collections.StringSliceToInterfaceSlice(args)
   631  	argsv = append(argsv, hexec.WithEnviron(c.environ))
   632  	argsv = append(argsv, hexec.WithStderr(io.MultiWriter(stderr, os.Stderr)))
   633  	argsv = append(argsv, hexec.WithStdout(stdout))
   634  	argsv = append(argsv, hexec.WithDir(c.ccfg.WorkingDir))
   635  	argsv = append(argsv, hexec.WithContext(ctx))
   636  
   637  	cmd, err := c.ccfg.Exec.New("go", argsv...)
   638  	if err != nil {
   639  		return err
   640  	}
   641  
   642  	if err := cmd.Run(); err != nil {
   643  		if ee, ok := err.(*exec.Error); ok && ee.Err == exec.ErrNotFound {
   644  			c.goBinaryStatus = goBinaryStatusNotFound
   645  			return nil
   646  		}
   647  
   648  		if strings.Contains(stderr.String(), "invalid version: unknown revision") {
   649  			// See https://github.com/gohugoio/hugo/issues/6825
   650  			c.logger.Println(`An unknown revision most likely means that someone has deleted the remote ref (e.g. with a force push to GitHub).
   651  To resolve this, you need to manually edit your go.mod file and replace the version for the module in question with a valid ref.
   652  
   653  The easiest is to just enter a valid branch name there, e.g. master, which would be what you put in place of 'v0.5.1' in the example below.
   654  
   655  require github.com/gohugoio/hugo-mod-jslibs/instantpage v0.5.1
   656  
   657  If you then run 'hugo mod graph' it should resolve itself to the most recent version (or commit if no semver versions are available).`)
   658  		}
   659  
   660  		_, ok := err.(*exec.ExitError)
   661  		if !ok {
   662  			return fmt.Errorf("failed to execute 'go %v': %s %T", args, err, err)
   663  		}
   664  
   665  		// Too old Go version
   666  		if strings.Contains(stderr.String(), "flag provided but not defined") {
   667  			c.goBinaryStatus = goBinaryStatusTooOld
   668  			return nil
   669  		}
   670  
   671  		return fmt.Errorf("go command failed: %s", stderr)
   672  
   673  	}
   674  
   675  	return nil
   676  }
   677  
   678  func (c *Client) tidy(mods Modules, goModOnly bool) error {
   679  	isGoMod := make(map[string]bool)
   680  	for _, m := range mods {
   681  		if m.Owner() == nil {
   682  			continue
   683  		}
   684  		if m.IsGoMod() {
   685  			// Matching the format in go.mod
   686  			pathVer := m.Path() + " " + m.Version()
   687  			isGoMod[pathVer] = true
   688  		}
   689  	}
   690  
   691  	if err := c.rewriteGoMod(goModFilename, isGoMod); err != nil {
   692  		return err
   693  	}
   694  
   695  	if goModOnly {
   696  		return nil
   697  	}
   698  
   699  	if err := c.rewriteGoMod(goSumFilename, isGoMod); err != nil {
   700  		return err
   701  	}
   702  
   703  	return nil
   704  }
   705  
   706  func (c *Client) shouldVendor(path string) bool {
   707  	return c.noVendor == nil || !c.noVendor.Match(path)
   708  }
   709  
   710  func (c *Client) createThemeDirname(modulePath string, isProjectMod bool) (string, error) {
   711  	invalid := fmt.Errorf("invalid module path %q; must be relative to themesDir when defined outside of the project", modulePath)
   712  
   713  	modulePath = filepath.Clean(modulePath)
   714  	if filepath.IsAbs(modulePath) {
   715  		if isProjectMod {
   716  			return modulePath, nil
   717  		}
   718  		return "", invalid
   719  	}
   720  
   721  	moduleDir := filepath.Join(c.ccfg.ThemesDir, modulePath)
   722  	if !isProjectMod && !strings.HasPrefix(moduleDir, c.ccfg.ThemesDir) {
   723  		return "", invalid
   724  	}
   725  	return moduleDir, nil
   726  }
   727  
   728  // ClientConfig configures the module Client.
   729  type ClientConfig struct {
   730  	Fs     afero.Fs
   731  	Logger loggers.Logger
   732  
   733  	// If set, it will be run before we do any duplicate checks for modules
   734  	// etc.
   735  	HookBeforeFinalize func(m *ModulesConfig) error
   736  
   737  	// Ignore any _vendor directory for module paths matching the given pattern.
   738  	// This can be nil.
   739  	IgnoreVendor glob.Glob
   740  
   741  	// Absolute path to the project dir.
   742  	WorkingDir string
   743  
   744  	// Absolute path to the project's themes dir.
   745  	ThemesDir string
   746  
   747  	// Eg. "production"
   748  	Environment string
   749  
   750  	Exec *hexec.Exec
   751  
   752  	CacheDir     string // Module cache
   753  	ModuleConfig Config
   754  }
   755  
   756  func (c ClientConfig) shouldIgnoreVendor(path string) bool {
   757  	return c.IgnoreVendor != nil && c.IgnoreVendor.Match(path)
   758  }
   759  
   760  type goBinaryStatus int
   761  
   762  type goModule struct {
   763  	Path     string         // module path
   764  	Version  string         // module version
   765  	Versions []string       // available module versions (with -versions)
   766  	Replace  *goModule      // replaced by this module
   767  	Time     *time.Time     // time version was created
   768  	Update   *goModule      // available update, if any (with -u)
   769  	Main     bool           // is this the main module?
   770  	Indirect bool           // is this module only an indirect dependency of main module?
   771  	Dir      string         // directory holding files for this module, if any
   772  	GoMod    string         // path to go.mod file for this module, if any
   773  	Error    *goModuleError // error loading module
   774  }
   775  
   776  type goModuleError struct {
   777  	Err string // the error itself
   778  }
   779  
   780  type goModules []*goModule
   781  
   782  func (modules goModules) GetByPath(p string) *goModule {
   783  	if modules == nil {
   784  		return nil
   785  	}
   786  
   787  	for _, m := range modules {
   788  		if strings.EqualFold(p, m.Path) {
   789  			return m
   790  		}
   791  	}
   792  
   793  	return nil
   794  }
   795  
   796  func (modules goModules) GetMain() *goModule {
   797  	for _, m := range modules {
   798  		if m.Main {
   799  			return m
   800  		}
   801  	}
   802  
   803  	return nil
   804  }
   805  
   806  func getModlineSplitter(isGoMod bool) func(line string) []string {
   807  	if isGoMod {
   808  		return func(line string) []string {
   809  			if strings.HasPrefix(line, "require (") {
   810  				return nil
   811  			}
   812  			if !strings.HasPrefix(line, "require") && !strings.HasPrefix(line, "\t") {
   813  				return nil
   814  			}
   815  			line = strings.TrimPrefix(line, "require")
   816  			line = strings.TrimSpace(line)
   817  			line = strings.TrimSuffix(line, "// indirect")
   818  
   819  			return strings.Fields(line)
   820  		}
   821  	}
   822  
   823  	return func(line string) []string {
   824  		return strings.Fields(line)
   825  	}
   826  }
   827  
   828  func pathVersion(m Module) string {
   829  	versionStr := m.Version()
   830  	if m.Vendor() {
   831  		versionStr += "+vendor"
   832  	}
   833  	if versionStr == "" {
   834  		return m.Path()
   835  	}
   836  	return fmt.Sprintf("%s@%s", m.Path(), versionStr)
   837  }