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