diff --git a/modfetch/fetch.go b/modfetch/fetch.go index d317493..1955fdc 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,19 +204,44 @@ 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 + } + if !found { + first, found = m, true + } + if reqPath != "" && m.Path == reqPath { + mod = m + break } - return getMod(data[len(downloading):], nil) } - err = xmod.ErrNotFound + if !found { + err = xmod.ErrNotFound + return + } + if mod.Path == "" { + mod = first + } + fmt.Fprintln(os.Stderr, "xgo: downloading", mod.Path, mod.Version) 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) + } +}