From 6ea2cb70e04e731404ad00d78a4a82dc8b50d5a7 Mon Sep 17 00:00:00 2001 From: Jakub Zych Date: Wed, 28 Oct 2026 19:42:20 +0100 Subject: [PATCH] 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. --- internal/build/scaffold.go | 19 +++++++++++ internal/build/toolchain_test.go | 58 ++++++++++++++++++++++++++++++++ 2 files changed, 77 insertions(+) create mode 100644 internal/build/toolchain_test.go diff --git a/internal/build/scaffold.go b/internal/build/scaffold.go index 6c48957c..07342f48 100644 --- a/internal/build/scaffold.go +++ b/internal/build/scaffold.go @@ -9,6 +9,7 @@ import ( "go/format" "go/parser" "go/token" + "go/version" "os" "os/exec" "path/filepath" @@ -555,6 +556,24 @@ func ensureToolchain(path string) error { return nil } 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) inserted := false for _, line := range lines { diff --git a/internal/build/toolchain_test.go b/internal/build/toolchain_test.go new file mode 100644 index 00000000..d660e98b --- /dev/null +++ b/internal/build/toolchain_test.go @@ -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) + } + }) + } +}