From d93b27290938c5309f8ba47abdfac6ece55c08f3 Mon Sep 17 00:00:00 2001 From: xushiwei <396972+xushiwei@users.noreply.github.com> Date: Mon, 28 Sep 2026 21:45:31 +0000 Subject: [PATCH 1/2] modfetch: select the requested module among multiple `go: downloading` lines When `go get @` targets a module that lives in a multi-module repository, `go` downloads the parent module as well and prints one `go: downloading ...` line per module. Their order is not deterministic. `getResult` previously returned the first line unconditionally, so it could return the parent module path (e.g. `github.com/llarhub/libcxx`) instead of the requested submodule (`github.com/llarhub/libcxx/c`). Downstream callers such as llcppg then resolved the wrong module cache directory, which failed intermittently depending on the download order and cache state. Scan all `go: downloading` lines and prefer the one whose module path matches the requested module, falling back to the first line to preserve the existing single-module behavior. Fixes goplus/llcppg#867 --- modfetch/fetch.go | 40 +++++++++++++++++++---- modfetch/fetch_test.go | 72 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 106 insertions(+), 6 deletions(-) create mode 100644 modfetch/fetch_test.go diff --git a/modfetch/fetch.go b/modfetch/fetch.go index d317493..243c828 100644 --- a/modfetch/fetch.go +++ b/modfetch/fetch.go @@ -189,7 +189,11 @@ func Get(modPath string, noCache ...bool) (mod module.Version, err error) { cmd.Stderr = &stderr cmd.Run() if stderr.Len() > 0 { - mod, err = getResult(stderr.String()) + reqPath := modPath + if pos := strings.IndexByte(reqPath, '@'); pos > 0 { + reqPath = reqPath[:pos] + } + mod, err = getResult(stderr.String(), reqPath) if err != xmod.ErrNotFound { if debugVerbose { log.Println("modfetch.Get ret:", err) @@ -200,17 +204,41 @@ func Get(modPath string, noCache ...bool) (mod module.Version, err error) { return getFromCache(modPath) } -func getResult(data string) (mod module.Version, err error) { +func getResult(data string, reqPath string) (mod module.Version, err error) { if debugVerbose { log.Println("modfetch.getResult:", data) } // go: downloading github.com/xushiwei/foogop v0.1.0 + // + // A single `go get` may emit multiple downloading lines (e.g. when the + // requested module lives in a multi-module repository, its parent module is + // downloaded too) and their order is not deterministic. Prefer the line + // whose module path exactly matches the requested one; fall back to the + // first downloading line to preserve the single-module behavior. const downloading = "go: downloading " - if strings.HasPrefix(data, downloading) { - if pos := strings.IndexByte(data, '\n'); pos > 0 { - fmt.Fprintln(os.Stderr, "xgo:", data[4:pos]) + var first module.Version + var found bool + for _, line := range strings.Split(data, "\n") { + if !strings.HasPrefix(line, downloading) { + continue + } + m, e := getMod(line[len(downloading):]+"\n", nil) + if e != nil { + continue } - return getMod(data[len(downloading):], nil) + if !found { + first, found = m, true + } + if reqPath != "" && m.Path == reqPath { + mod = m + fmt.Fprintln(os.Stderr, "xgo: downloading", mod.Path, mod.Version) + return + } + } + if found { + mod = first + fmt.Fprintln(os.Stderr, "xgo: downloading", mod.Path, mod.Version) + return } err = xmod.ErrNotFound return diff --git a/modfetch/fetch_test.go b/modfetch/fetch_test.go new file mode 100644 index 0000000..42e5a99 --- /dev/null +++ b/modfetch/fetch_test.go @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2021 The XGo Authors (xgo.dev). All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package modfetch + +import ( + "testing" + + xmod "github.com/goplus/mod" +) + +func TestGetResult(t *testing.T) { + const parentFirst = "go: downloading github.com/llarhub/libcxx v0.1.1\n" + + "go: downloading github.com/llarhub/libcxx/c v0.1.1\n" + + "go: added github.com/llarhub/libcxx/c v0.1.1\n" + const childFirst = "go: downloading github.com/llarhub/libcxx/c v0.1.1\n" + + "go: downloading github.com/llarhub/libcxx v0.1.1\n" + + "go: added github.com/llarhub/libcxx/c v0.1.1\n" + + // Whatever the order of the downloading lines, the requested module must be + // selected instead of the first-printed one (which may be its parent module + // in a multi-module repository). This ordering is non-deterministic in + // `go get`, which is the root cause of the intermittent failure this fixes. + for name, data := range map[string]string{ + "parentFirst": parentFirst, + "childFirst": childFirst, + } { + t.Run(name, func(t *testing.T) { + mod, err := getResult(data, "github.com/llarhub/libcxx/c") + if err != nil { + t.Fatal("getResult:", err) + } + if mod.Path != "github.com/llarhub/libcxx/c" || mod.Version != "v0.1.1" { + t.Fatalf("getResult: got %v %v", mod.Path, mod.Version) + } + }) + } +} + +func TestGetResultFallback(t *testing.T) { + // No exact match for reqPath: fall back to the first downloading line, and + // with an empty reqPath (single-module callers) keep the first line too. + const data = "go: downloading github.com/xushiwei/foogop v0.1.0\n" + for _, reqPath := range []string{"", "github.com/not/matched"} { + mod, err := getResult(data, reqPath) + if err != nil { + t.Fatal("getResult:", err) + } + if mod.Path != "github.com/xushiwei/foogop" || mod.Version != "v0.1.0" { + t.Fatalf("getResult(%q): got %v %v", reqPath, mod.Path, mod.Version) + } + } +} + +func TestGetResultNotFound(t *testing.T) { + if _, err := getResult("go: added something\n", ""); err != xmod.ErrNotFound { + t.Fatal("getResult: expected ErrNotFound, got", err) + } +} From 8c7bdf5687f3071eb05a62e40c6e10737db520bf Mon Sep 17 00:00:00 2001 From: xushiwei <396972+xushiwei@users.noreply.github.com> Date: Mon, 28 Sep 2026 22:06:40 +0000 Subject: [PATCH 2/2] modfetch: hoist fmt.Fprintln out of both branches Remove duplication by setting mod in both the exact-match and fallback paths, then emitting a single fmt.Fprintln after the branch. --- modfetch/fetch.go | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/modfetch/fetch.go b/modfetch/fetch.go index 243c828..1955fdc 100644 --- a/modfetch/fetch.go +++ b/modfetch/fetch.go @@ -231,16 +231,17 @@ func getResult(data string, reqPath string) (mod module.Version, err error) { } if reqPath != "" && m.Path == reqPath { mod = m - fmt.Fprintln(os.Stderr, "xgo: downloading", mod.Path, mod.Version) - return + break } } - if found { - mod = first - fmt.Fprintln(os.Stderr, "xgo: downloading", mod.Path, mod.Version) + if !found { + err = xmod.ErrNotFound return } - err = xmod.ErrNotFound + if mod.Path == "" { + mod = first + } + fmt.Fprintln(os.Stderr, "xgo: downloading", mod.Path, mod.Version) return }