diff --git a/internal/modindex/dir_test.go b/internal/modindex/dir_test.go new file mode 100644 index 000000000..862d111ea --- /dev/null +++ b/internal/modindex/dir_test.go @@ -0,0 +1,127 @@ +// Copyright 2024 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package modindex + +import ( + "os" + "path/filepath" + "testing" +) + +type id struct { + importPath string + best int // which of the dirs is the one that should have been chosen + dirs []string +} + +var idtests = []id{ + { // get one right + importPath: "cloud.google.com/go/longrunning", + best: 2, + dirs: []string{ + "cloud.google.com/go/longrunning@v0.3.0", + "cloud.google.com/go/longrunning@v0.4.1", + "cloud.google.com/go@v0.104.0/longrunning", + "cloud.google.com/go@v0.94.0/longrunning", + }, + }, + { // make sure we can run more than one test + importPath: "cloud.google.com/go/compute/metadata", + best: 2, + dirs: []string{ + "cloud.google.com/go/compute/metadata@v0.2.1", + "cloud.google.com/go/compute/metadata@v0.2.3", + "cloud.google.com/go/compute@v1.7.0/metadata", + "cloud.google.com/go@v0.94.0/compute/metadata", + }, + }, + { //m test bizarre characters in directory name + importPath: "bad,guy.com/go", + best: 0, + dirs: []string{"bad,guy.com/go@v0.1.0"}, + }, +} + +func testModCache(t *testing.T) string { + t.Helper() + dir := t.TempDir() + IndexDir = func() (string, error) { return dir, nil } + return dir +} + +func TestDirsSinglePath(t *testing.T) { + for _, itest := range idtests { + t.Run(itest.importPath, func(t *testing.T) { + // create a new fake GOMODCACHE + dir := testModCache(t) + for _, d := range itest.dirs { + if err := os.MkdirAll(filepath.Join(dir, d), 0755); err != nil { + t.Fatal(err) + } + // gopathwalk wants to see .go files + err := os.WriteFile(filepath.Join(dir, d, "main.go"), []byte("package main\nfunc main() {}"), 0600) + if err != nil { + t.Fatal(err) + } + } + // build and check the index + if err := IndexModCache(dir, false); err != nil { + t.Fatal(err) + } + ix, err := ReadIndex(dir) + if err != nil { + t.Fatal(err) + } + if len(ix.Entries) != 1 { + t.Fatalf("got %d entries, wanted 1", len(ix.Entries)) + } + if ix.Entries[0].ImportPath != itest.importPath { + t.Fatalf("got %s import path, wanted %s", ix.Entries[0].ImportPath, itest.importPath) + } + if ix.Entries[0].Dir != Relpath(itest.dirs[itest.best]) { + t.Fatalf("got dir %s, wanted %s", ix.Entries[0].Dir, itest.dirs[itest.best]) + } + }) + } +} + +/* more data for tests + +directories.go:169: WEIRD cloud.google.com/go/iam/admin/apiv1 +map[cloud.google.com/go:1 cloud.google.com/go/iam:5]: +[cloud.google.com/go/iam@v0.12.0/admin/apiv1 +cloud.google.com/go/iam@v0.13.0/admin/apiv1 +cloud.google.com/go/iam@v0.3.0/admin/apiv1 +cloud.google.com/go/iam@v0.7.0/admin/apiv1 +cloud.google.com/go/iam@v1.0.1/admin/apiv1 +cloud.google.com/go@v0.94.0/iam/admin/apiv1] +directories.go:169: WEIRD cloud.google.com/go/iam +map[cloud.google.com/go:1 cloud.google.com/go/iam:5]: +[cloud.google.com/go/iam@v0.12.0 cloud.google.com/go/iam@v0.13.0 +cloud.google.com/go/iam@v0.3.0 cloud.google.com/go/iam@v0.7.0 +cloud.google.com/go/iam@v1.0.1 cloud.google.com/go@v0.94.0/iam] +directories.go:169: WEIRD cloud.google.com/go/compute/apiv1 +map[cloud.google.com/go:1 cloud.google.com/go/compute:4]: +[cloud.google.com/go/compute@v1.12.1/apiv1 +cloud.google.com/go/compute@v1.18.0/apiv1 +cloud.google.com/go/compute@v1.19.0/apiv1 +cloud.google.com/go/compute@v1.7.0/apiv1 +cloud.google.com/go@v0.94.0/compute/apiv1] +directories.go:169: WEIRD cloud.google.com/go/longrunning/autogen +map[cloud.google.com/go:2 cloud.google.com/go/longrunning:2]: +[cloud.google.com/go/longrunning@v0.3.0/autogen +cloud.google.com/go/longrunning@v0.4.1/autogen +cloud.google.com/go@v0.104.0/longrunning/autogen +cloud.google.com/go@v0.94.0/longrunning/autogen] +directories.go:169: WEIRD cloud.google.com/go/iam/credentials/apiv1 +map[cloud.google.com/go:1 cloud.google.com/go/iam:5]: +[cloud.google.com/go/iam@v0.12.0/credentials/apiv1 +cloud.google.com/go/iam@v0.13.0/credentials/apiv1 +cloud.google.com/go/iam@v0.3.0/credentials/apiv1 +cloud.google.com/go/iam@v0.7.0/credentials/apiv1 +cloud.google.com/go/iam@v1.0.1/credentials/apiv1 +cloud.google.com/go@v0.94.0/iam/credentials/apiv1] + +*/ diff --git a/internal/modindex/directories.go b/internal/modindex/directories.go new file mode 100644 index 000000000..b8aab3b73 --- /dev/null +++ b/internal/modindex/directories.go @@ -0,0 +1,137 @@ +// Copyright 2024 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package modindex + +import ( + "fmt" + "log" + "os" + "path/filepath" + "regexp" + "slices" + "strings" + "sync" + "time" + + "golang.org/x/mod/semver" + "golang.org/x/tools/internal/gopathwalk" +) + +type directory struct { + path Relpath + importPath string + version string // semantic version +} + +// filterDirs groups the directories by import path, +// sorting the ones with the same import path by semantic version, +// most recent first. +func byImportPath(dirs []Relpath) (map[string][]*directory, error) { + ans := make(map[string][]*directory) // key is import path + for _, d := range dirs { + ip, sv, err := DirToImportPathVersion(d) + if err != nil { + return nil, err + } + ans[ip] = append(ans[ip], &directory{ + path: d, + importPath: ip, + version: sv, + }) + } + for k, v := range ans { + semanticSort(v) + ans[k] = v + } + return ans, nil +} + +// sort the directories by semantic version, lates first +func semanticSort(v []*directory) { + slices.SortFunc(v, func(l, r *directory) int { + if n := semver.Compare(l.version, r.version); n != 0 { + return -n // latest first + } + return strings.Compare(string(l.path), string(r.path)) + }) +} + +// modCacheRegexp splits a relpathpath into module, module version, and package. +var modCacheRegexp = regexp.MustCompile(`(.*)@([^/\\]*)(.*)`) + +// DirToImportPathVersion computes import path and semantic version +func DirToImportPathVersion(dir Relpath) (string, string, error) { + m := modCacheRegexp.FindStringSubmatch(string(dir)) + // m[1] is the module path + // m[2] is the version major.minor.patch(-
that contains the name +// of the current index. We believe writing that short file is atomic. +// ReadIndex reads that file to get the file name of the index. +// WriteIndex writes an index with a unique name and then +// writes that name into a new version of index-name-. +// ( stands for the CurrentVersion of the index format.) +package modindex + +import ( + "log" + "path/filepath" + "slices" + "strings" + "time" + + "golang.org/x/mod/semver" +) + +// Modindex writes an index current as of when it is called. +// If clear is true the index is constructed from all of GOMODCACHE +// otherwise the index is constructed from the last previous index +// and the updates to the cache. +func IndexModCache(cachedir string, clear bool) error { + cachedir, err := filepath.Abs(cachedir) + if err != nil { + return err + } + cd := Abspath(cachedir) + future := time.Now().Add(24 * time.Hour) // safely in the future + err = modindexTimed(future, cd, clear) + if err != nil { + return err + } + return nil +} + +// modindexTimed writes an index current as of onlyBefore. +// If clear is true the index is constructed from all of GOMODCACHE +// otherwise the index is constructed from the last previous index +// and all the updates to the cache before onlyBefore. +// (this is useful for testing; perhaps it should not be exported) +func modindexTimed(onlyBefore time.Time, cachedir Abspath, clear bool) error { + var curIndex *Index + if !clear { + var err error + curIndex, err = ReadIndex(string(cachedir)) + if clear && err != nil { + return err + } + // TODO(pjw): check that most of those directorie still exist + } + cfg := &work{ + onlyBefore: onlyBefore, + oldIndex: curIndex, + cacheDir: cachedir, + } + if curIndex != nil { + cfg.onlyAfter = curIndex.Changed + } + if err := cfg.buildIndex(); err != nil { + return err + } + if err := cfg.writeIndex(); err != nil { + return err + } + return nil +} + +type work struct { + onlyBefore time.Time // do not use directories later than this + onlyAfter time.Time // only interested in directories after this + // directories from before onlyAfter come from oldIndex + oldIndex *Index + newIndex *Index + cacheDir Abspath +} + +func (w *work) buildIndex() error { + // The effective date of the new index should be at least + // slightly earlier than when the directories are scanned + // so set it now. + w.newIndex = &Index{Changed: time.Now(), Cachedir: w.cacheDir} + dirs := findDirs(string(w.cacheDir), w.onlyAfter, w.onlyBefore) + newdirs, err := byImportPath(dirs) + if err != nil { + return err + } + log.Printf("%d dirs, %d ips", len(dirs), len(newdirs)) + // for each import path it might occur only in newdirs, + // only in w.oldIndex, or in both. + // If it occurs in both, use the semantically later one + if w.oldIndex != nil { + killed := 0 + for _, e := range w.oldIndex.Entries { + found, ok := newdirs[e.ImportPath] + if !ok { + continue + } + if semver.Compare(found[0].version, e.Version) > 0 { + // the new one is better, disable the old one + e.ImportPath = "" + killed++ + } else { + // use the old one, forget the new one + delete(newdirs, e.ImportPath) + } + } + log.Printf("%d killed, %d ips", killed, len(newdirs)) + } + // Build the skeleton of the new index using newdirs, + // and include the surviving parts of the old index + if w.oldIndex != nil { + for _, e := range w.oldIndex.Entries { + if e.ImportPath != "" { + w.newIndex.Entries = append(w.newIndex.Entries, e) + } + } + } + for k, v := range newdirs { + d := v[0] + entry := Entry{ + Dir: d.path, + ImportPath: k, + Version: d.version, + } + w.newIndex.Entries = append(w.newIndex.Entries, entry) + } + // find symbols for the incomplete entries + log.Print("not finding any symbols yet") + // sort the entries in the new index + slices.SortFunc(w.newIndex.Entries, func(l, r Entry) int { + if n := strings.Compare(l.PkgName, r.PkgName); n != 0 { + return n + } + return strings.Compare(l.ImportPath, r.ImportPath) + }) + return nil +} + +func (w *work) writeIndex() error { + return writeIndex(w.cacheDir, w.newIndex) +} diff --git a/internal/modindex/types.go b/internal/modindex/types.go new file mode 100644 index 000000000..ece448863 --- /dev/null +++ b/internal/modindex/types.go @@ -0,0 +1,25 @@ +// Copyright 2024 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package modindex + +import ( + "strings" +) + +// some special types to avoid confusions + +// distinguish various types of directory names. It's easy to get confused. +type Abspath string // absolute paths +type Relpath string // paths with GOMODCACHE prefix removed + +func toRelpath(cachedir Abspath, s string) Relpath { + if strings.HasPrefix(s, string(cachedir)) { + if s == string(cachedir) { + return Relpath("") + } + return Relpath(s[len(cachedir)+1:]) + } + return Relpath(s) +}