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