fix(build): keep an existing toolchain line instead of adding a second one

make:*, plugin:add and new inserted toolchain go1.27.0 into go.mod and
go.work whenever that exact line was missing. A file that already named
another toolchain (go mod tidy and go work use write the running one)
ended up with two toolchain lines, and Go refused to load it with
"repeated toolchain statement". ensureToolchain now keeps an existing
line, replaces it only when it names an older toolchain, and inserts
one only when there is none.
This commit is contained in:
Jakub Zych
2026-10-28 19:42:20 +01:00
parent 8c9c967c88
commit 6ea2cb70e0
2 changed files with 77 additions and 0 deletions

View File

@@ -9,6 +9,7 @@ import (
"go/format" "go/format"
"go/parser" "go/parser"
"go/token" "go/token"
"go/version"
"os" "os"
"os/exec" "os/exec"
"path/filepath" "path/filepath"
@@ -555,6 +556,24 @@ func ensureToolchain(path string) error {
return nil return nil
} }
lines := strings.Split(string(data), "\n") lines := strings.Split(string(data), "\n")
// A file may carry its own toolchain line (go mod tidy and go work use
// write the running toolchain). Go refuses a second one, so keep an
// existing line unless it names an older toolchain than ours.
want := strings.TrimSpace(strings.TrimPrefix(toolchainLine, "toolchain"))
for i, line := range lines {
fields := strings.Fields(line)
if len(fields) != 2 || fields[0] != "toolchain" {
continue
}
if version.Compare(fields[1], want) >= 0 {
return nil
}
lines[i] = toolchainLine
if err := os.WriteFile(path, []byte(strings.Join(lines, "\n")), 0o644); err != nil {
return fmt.Errorf("build: write %s: %w", path, err)
}
return nil
}
out := make([]string, 0, len(lines)+2) out := make([]string, 0, len(lines)+2)
inserted := false inserted := false
for _, line := range lines { for _, line := range lines {

View File

@@ -0,0 +1,58 @@
package build
import (
"os"
"path/filepath"
"strings"
"testing"
)
func TestEnsureToolchain(t *testing.T) {
cases := []struct {
name string
in string
want string
}{
{
name: "missing line is added after go",
in: "go 1.27\n\nuse .\n",
want: "go 1.27\n\n" + toolchainLine + "\n\nuse .\n",
},
{
name: "our line is kept",
in: "go 1.27\n\n" + toolchainLine + "\n",
want: "go 1.27\n\n" + toolchainLine + "\n",
},
{
name: "newer toolchain is kept, not repeated",
in: "go 1.27\n\ntoolchain go1.99.2\n\nuse .\n",
want: "go 1.27\n\ntoolchain go1.99.2\n\nuse .\n",
},
{
name: "older toolchain is replaced",
in: "module x\n\ngo 1.25\n\ntoolchain go1.25.1\n",
want: "module x\n\ngo 1.25\n\n" + toolchainLine + "\n",
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
path := filepath.Join(t.TempDir(), "go.work")
if err := os.WriteFile(path, []byte(tc.in), 0o644); err != nil {
t.Fatal(err)
}
if err := ensureToolchain(path); err != nil {
t.Fatalf("ensureToolchain: %v", err)
}
got, err := os.ReadFile(path)
if err != nil {
t.Fatal(err)
}
if string(got) != tc.want {
t.Fatalf("got:\n%s\nwant:\n%s", got, tc.want)
}
if n := strings.Count(string(got), "\ntoolchain "); n != 1 {
t.Fatalf("got %d toolchain lines, want 1", n)
}
})
}
}