directories.go (3754B)
1 // Copyright 2024 The Go Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style 3 // license that can be found in the LICENSE file. 4 5 package modindex 6 7 import ( 8 "fmt" 9 "log" 10 "os" 11 "path/filepath" 12 "regexp" 13 "strings" 14 "sync" 15 "time" 16 17 "golang.org/x/mod/semver" 18 "golang.org/x/tools/internal/gopathwalk" 19 ) 20 21 type directory struct { 22 path string // relative to GOMODCACHE 23 importPath string 24 version string // semantic version 25 } 26 27 // bestDirByImportPath returns the best directory for each import 28 // path, where "best" means most recent semantic version. These import 29 // paths are inferred from the GOMODCACHE-relative dir names in dirs. 30 func bestDirByImportPath(dirs []string) (map[string]directory, error) { 31 dirsByPath := make(map[string]directory) 32 for _, dir := range dirs { 33 importPath, version, err := dirToImportPathVersion(dir) 34 if err != nil { 35 return nil, err 36 } 37 new := directory{ 38 path: dir, 39 importPath: importPath, 40 version: version, 41 } 42 if old, ok := dirsByPath[importPath]; !ok || compareDirectory(new, old) < 0 { 43 dirsByPath[importPath] = new 44 } 45 } 46 return dirsByPath, nil 47 } 48 49 // compareDirectory defines an ordering of path@version directories, 50 // by descending version, then by ascending path. 51 func compareDirectory(x, y directory) int { 52 if sign := -semver.Compare(x.version, y.version); sign != 0 { 53 return sign // latest first 54 } 55 return strings.Compare(string(x.path), string(y.path)) 56 } 57 58 // modCacheRegexp splits a relpathpath into module, module version, and package. 59 var modCacheRegexp = regexp.MustCompile(`(.*)@([^/\\]*)(.*)`) 60 61 // dirToImportPathVersion computes import path and semantic version 62 // from a GOMODCACHE-relative directory name. 63 func dirToImportPathVersion(dir string) (string, string, error) { 64 m := modCacheRegexp.FindStringSubmatch(string(dir)) 65 // m[1] is the module path 66 // m[2] is the version major.minor.patch(-<pre release identifier) 67 // m[3] is the rest of the package path 68 if len(m) != 4 { 69 return "", "", fmt.Errorf("bad dir %s", dir) 70 } 71 if !semver.IsValid(m[2]) { 72 return "", "", fmt.Errorf("bad semantic version %s", m[2]) 73 } 74 // ToSlash is required to convert Windows file paths 75 // into Go package import paths. 76 return filepath.ToSlash(m[1] + m[3]), m[2], nil 77 } 78 79 // findDirs returns an unordered list of relevant package directories, 80 // relative to the specified module cache root. The result includes only 81 // module dirs whose mtime is within (start, end). 82 func findDirs(root string, start, end time.Time) []string { 83 var ( 84 resMu sync.Mutex 85 res []string 86 ) 87 88 addDir := func(root gopathwalk.Root, dir string) { 89 // TODO(pjw): do we need to check times? 90 resMu.Lock() 91 defer resMu.Unlock() 92 res = append(res, relative(root.Path, dir)) 93 } 94 95 skipDir := func(_ gopathwalk.Root, dir string) bool { 96 // The cache directory is already ignored in gopathwalk. 97 if filepath.Base(dir) == "internal" { 98 return true 99 } 100 101 // Skip toolchains. 102 if strings.Contains(dir, "toolchain@") { 103 return true 104 } 105 106 // Don't look inside @ directories that are too old/new. 107 if strings.Contains(filepath.Base(dir), "@") { 108 st, err := os.Stat(dir) 109 if err != nil { 110 log.Printf("can't stat dir %s %v", dir, err) 111 return true 112 } 113 mtime := st.ModTime() 114 return mtime.Before(start) || mtime.After(end) 115 } 116 117 return false 118 } 119 120 // TODO(adonovan): parallelize this. Even with a hot buffer cache, 121 // find $(go env GOMODCACHE) -type d 122 // can easily take up a minute. 123 roots := []gopathwalk.Root{{Path: root, Type: gopathwalk.RootModuleCache}} 124 gopathwalk.WalkSkip(roots, addDir, skipDir, gopathwalk.Options{ 125 ModulesEnabled: true, 126 Concurrency: 1, // TODO(pjw): adjust concurrency 127 // Logf: log.Printf, 128 }) 129 130 return res 131 }