tools/bump: upgrade modules behind tool directives

x/tools is indirect, so never moved; v0.49.0 cannot read Go 1.27.2
export data and viewer emits invalid types, failing go generate.
This commit is contained in:
Kristoffer Dalby committed 2026-10-15 10:50:32 +02:00
1 parent bf63feb320
commit fd09dd86f2
2 files changed
+65 -2

No files matched your search

+24 -2
View File
@@ -166,7 +166,9 @@ func sqliteAtom(ctx context.Context, r *repo) (string, error) {
}
// directRequirements is every direct requirement not already owned by a
// lockstep atom.
// lockstep atom, plus the modules behind go.mod's tool lines. Those are marked
// indirect when nothing imports them, yet go generate runs them, and a stale
// golang.org/x/tools cannot read the export data a newer Go writes.
func directRequirements(r *repo) ([]string, error) {
f, err := parseGoMod(r)
if err != nil {
@@ -181,10 +183,15 @@ func directRequirements(r *repo) ([]string, error) {
modLibc: true,
}
tools := map[string]bool{}
for _, t := range f.Tool {
tools[toolModule(f, t.Path)] = true
}
var paths []string
for _, req := range f.Require {
if req.Indirect || owned[req.Mod.Path] {
if owned[req.Mod.Path] || (req.Indirect && !tools[req.Mod.Path]) {
continue
}
@@ -194,6 +201,21 @@ func directRequirements(r *repo) ([]string, error) {
return paths, nil
}
// toolModule finds the required module providing pkg. The longest match wins,
// since a nested module shadows its parent's tree.
func toolModule(f *modfile.File, pkg string) string {
var best string
for _, req := range f.Require {
p := req.Mod.Path
if (pkg == p || strings.HasPrefix(pkg, p+"/")) && len(p) > len(best) {
best = p
}
}
return best
}
// moduleAtom upgrades one direct requirement. One atom per module costs nothing
// on a good day, because the bisect tries every atom together first and only
// splits when that fails. It is what stops a single dependency deprecating an
+41
View File
@@ -1,6 +1,9 @@
package main
import (
"os"
"path/filepath"
"slices"
"testing"
"golang.org/x/mod/modfile"
@@ -117,3 +120,41 @@ func TestRepoLockstepNotesAttached(t *testing.T) {
t.Error("go.mod: tool block is missing")
}
}
// A tool's module is indirect in go.mod, but it still has to move: go generate
// runs it against whatever Go the flake ships.
func TestDirectRequirementsIncludesTools(t *testing.T) {
const content = `module example.com/x
go 1.27.0
require (
github.com/go-chi/chi/v5 v5.4.0
golang.org/x/net v0.58.0 // indirect
golang.org/x/tools v0.49.0 // indirect
tailscale.com v1.100.0
)
tool (
golang.org/x/tools/cmd/stringer
tailscale.com/cmd/viewer
)
`
dir := t.TempDir()
err := os.WriteFile(filepath.Join(dir, "go.mod"), []byte(content), 0o600)
if err != nil {
t.Fatal(err)
}
got, err := directRequirements(&repo{Root: dir})
if err != nil {
t.Fatal(err)
}
want := []string{"github.com/go-chi/chi/v5", "golang.org/x/tools"}
if !slices.Equal(got, want) {
t.Errorf("directRequirements = %v, want %v", got, want)
}
}