diff --git a/Documentation/cli/README.md b/Documentation/cli/README.md index 7cf266dd0b..4efd68c078 100644 --- a/Documentation/cli/README.md +++ b/Documentation/cli/README.md @@ -256,10 +256,13 @@ Changes the value of a configuration parameter. config substitute-path config substitute-path config substitute-path -clear + config substitute-path -guess Adds or removes a path substitution rule, if -clear is used all substitute-path rules are removed. Without arguments shows the current list of substitute-path rules. +The -guess option causes Delve to try to guess your substitute-path +configuration automatically. See also [Documentation/cli/substitutepath.md](//github.com/go-delve/delve/tree/master/Documentation/cli/substitutepath.md) for how the rules are applied. config alias diff --git a/Documentation/cli/starlark.md b/Documentation/cli/starlark.md index edb878dd35..da2b4cdd92 100644 --- a/Documentation/cli/starlark.md +++ b/Documentation/cli/starlark.md @@ -44,6 +44,7 @@ function_return_locations(FnName) | Equivalent to API call [FunctionReturnLocati get_breakpoint(Id, Name) | Equivalent to API call [GetBreakpoint](https://pkg.go.dev/github.com/go-delve/delve/service/rpc2#RPCServer.GetBreakpoint) get_buffered_tracepoints() | Equivalent to API call [GetBufferedTracepoints](https://pkg.go.dev/github.com/go-delve/delve/service/rpc2#RPCServer.GetBufferedTracepoints) get_thread(Id) | Equivalent to API call [GetThread](https://pkg.go.dev/github.com/go-delve/delve/service/rpc2#RPCServer.GetThread) +guess_substitute_path(ModuleDirectories) | Equivalent to API call [GuessSubstitutePath](https://pkg.go.dev/github.com/go-delve/delve/service/rpc2#RPCServer.GuessSubstitutePath) is_multiclient() | Equivalent to API call [IsMulticlient](https://pkg.go.dev/github.com/go-delve/delve/service/rpc2#RPCServer.IsMulticlient) last_modified() | Equivalent to API call [LastModified](https://pkg.go.dev/github.com/go-delve/delve/service/rpc2#RPCServer.LastModified) breakpoints(All) | Equivalent to API call [ListBreakpoints](https://pkg.go.dev/github.com/go-delve/delve/service/rpc2#RPCServer.ListBreakpoints) diff --git a/Documentation/usage/dlv_substitute-path-guess-helper.md b/Documentation/usage/dlv_substitute-path-guess-helper.md new file mode 100644 index 0000000000..4a0c1ba548 --- /dev/null +++ b/Documentation/usage/dlv_substitute-path-guess-helper.md @@ -0,0 +1,39 @@ +## dlv substitute-path-guess-helper + + + +``` +dlv substitute-path-guess-helper [flags] +``` + +### Options + +``` + -h, --help help for substitute-path-guess-helper +``` + +### Options inherited from parent commands + +``` + --accept-multiclient Allows a headless server to accept multiple client connections via JSON-RPC or DAP. + --allow-non-terminal-interactive Allows interactive sessions of Delve that don't have a terminal as stdin, stdout and stderr + --api-version int Selects JSON-RPC API version when headless. New clients should use v2. Can be reset via RPCServer.SetApiVersion. See Documentation/api/json-rpc/README.md. (default 1) + --backend string Backend selection (see 'dlv help backend'). (default "default") + --build-flags string Build flags, to be passed to the compiler. For example: --build-flags="-tags=integration -mod=vendor -cover -v" + --check-go-version Exits if the version of Go in use is not compatible (too old or too new) with the version of Delve. (default true) + --disable-aslr Disables address space randomization + --headless Run debug server only, in headless mode. Server will accept both JSON-RPC or DAP client connections. + --init string Init file, executed by the terminal client. + -l, --listen string Debugging server listen address. Prefix with 'unix:' to use a unix domain socket. (default "127.0.0.1:0") + --log Enable debugging server logging. + --log-dest string Writes logs to the specified file or file descriptor (see 'dlv help log'). + --log-output string Comma separated list of components that should produce debug output (see 'dlv help log') + --only-same-user Only connections from the same user that started this instance of Delve are allowed to connect. (default true) + -r, --redirect stringArray Specifies redirect rules for target process (see 'dlv help redirect') + --wd string Working directory for running the program. +``` + +### SEE ALSO + +* [dlv](dlv.md) - Delve is a debugger for the Go programming language. + diff --git a/_scripts/make.go b/_scripts/make.go index 2a7442faf6..78d99440da 100644 --- a/_scripts/make.go +++ b/_scripts/make.go @@ -130,6 +130,9 @@ This option can only be specified if testset is basic or a single package.`) } func checkCert() bool { + if os.Getenv("NOCERT") != "" { + return false + } // If we're on OSX make sure the proper CERT env var is set. if runtime.GOOS != "darwin" || os.Getenv("CERT") != "" { return true @@ -321,7 +324,7 @@ func buildFlags() []string { } else { ldFlags = "-X main.Build=" + buildSHA } - if runtime.GOOS == "darwin" { + if runtime.GOOS == "darwin" && os.Getenv("CERT") != "" { ldFlags = "-s " + ldFlags } return []string{fmt.Sprintf("-ldflags=%s", ldFlags)} diff --git a/cmd/dlv/cmds/commands.go b/cmd/dlv/cmds/commands.go index 95b294f040..06df0b078d 100644 --- a/cmd/dlv/cmds/commands.go +++ b/cmd/dlv/cmds/commands.go @@ -512,6 +512,24 @@ File redirects can also be changed using the 'restart' command. `, }) + rootCommand.AddCommand(&cobra.Command{ + Use: "substitute-path-guess-helper", + Hidden: true, + Run: func(cmd *cobra.Command, args []string) { + gsp, err := rpc2.MakeGuessSusbtitutePathIn() + if err != nil { + fmt.Printf("ERROR: %v\n", err) + os.Exit(1) + } + err = json.NewEncoder(os.Stdout).Encode(gsp) + if err != nil { + fmt.Printf("ERROR: %v\n", err) + os.Exit(1) + } + os.Exit(0) + }, + }) + rootCommand.DisableAutoGenTag = true configUsageFunc(rootCommand) diff --git a/cmd/dlv/dlv_test.go b/cmd/dlv/dlv_test.go index c9fd264102..ebad05e82f 100644 --- a/cmd/dlv/dlv_test.go +++ b/cmd/dlv/dlv_test.go @@ -61,45 +61,18 @@ func assertNoError(err error, t testing.TB, s string) { } } -func projectRoot() string { - wd, err := os.Getwd() - if err != nil { - panic(err) - } - - gopaths := strings.FieldsFunc(os.Getenv("GOPATH"), func(r rune) bool { return r == os.PathListSeparator }) - for _, curpath := range gopaths { - // Detects "gopath mode" when GOPATH contains several paths ex. "d:\\dir\\gopath;f:\\dir\\gopath2" - if strings.Contains(wd, curpath) { - return filepath.Join(curpath, "src", "github.com", "go-delve", "delve") - } - } - val, err := exec.Command("go", "list", "-mod=", "-m", "-f", "{{ .Dir }}").Output() - if err != nil { - panic(err) // the Go tool was tested to work earlier - } - return strings.TrimSuffix(string(val), "\n") -} - func TestBuild(t *testing.T) { const listenAddr = "127.0.0.1:40573" var err error - cmd := exec.Command("go", "run", "_scripts/make.go", "build") - cmd.Dir = projectRoot() - out, err := cmd.CombinedOutput() - if err != nil { - t.Fatalf("makefile error: %v\noutput %s\n", err, string(out)) - } - - dlvbin := filepath.Join(cmd.Dir, "dlv") + dlvbin := protest.GetDlvBinary(t) defer os.Remove(dlvbin) fixtures := protest.FindFixturesDir() buildtestdir := filepath.Join(fixtures, "buildtest") - cmd = exec.Command(dlvbin, "debug", "--headless=true", "--listen="+listenAddr, "--api-version=2", "--backend="+testBackend, "--log", "--log-output=debugger,rpc") + cmd := exec.Command(dlvbin, "debug", "--headless=true", "--listen="+listenAddr, "--api-version=2", "--backend="+testBackend, "--log", "--log-output=debugger,rpc") cmd.Dir = buildtestdir stderr, err := cmd.StderrPipe() assertNoError(err, t, "stderr pipe") @@ -344,7 +317,7 @@ func TestRedirect(t *testing.T) { const checkAutogenDocLongOutput = false func checkAutogenDoc(t *testing.T, filename, gencommand string, generated []byte) { - saved := slurpFile(t, filepath.Join(projectRoot(), filename)) + saved := slurpFile(t, filepath.Join(protest.ProjectRoot(), filename)) saved = bytes.ReplaceAll(saved, []byte("\r\n"), []byte{'\n'}) generated = bytes.ReplaceAll(generated, []byte("\r\n"), []byte{'\n'}) @@ -382,7 +355,7 @@ func diffMaybe(t *testing.T, filename string, generated []byte) { return } cmd := exec.Command("diff", filename, "-") - cmd.Dir = projectRoot() + cmd.Dir = protest.ProjectRoot() stdin, _ := cmd.StdinPipe() go func() { stdin.Write(generated) @@ -412,7 +385,7 @@ func TestGeneratedDoc(t *testing.T) { // Checks gen-usage-docs.go tempDir := t.TempDir() cmd := exec.Command("go", "run", "_scripts/gen-usage-docs.go", tempDir) - cmd.Dir = projectRoot() + cmd.Dir = protest.ProjectRoot() err := cmd.Run() assertNoError(err, t, "go run _scripts/gen-usage-docs.go") entries, err := os.ReadDir(tempDir) @@ -426,7 +399,7 @@ func TestGeneratedDoc(t *testing.T) { a := []string{"run"} a = append(a, args...) cmd := exec.Command("go", a...) - cmd.Dir = projectRoot() + cmd.Dir = protest.ProjectRoot() out, err := cmd.CombinedOutput() if err != nil { t.Fatalf("could not run script %v: %v (output: %q)", args, err, string(out)) @@ -1383,7 +1356,7 @@ func TestStaticcheck(t *testing.T) { // where we don't do this it is a deliberate style choice. // * ST1023 "Redundant type in variable declaration" same as S1021. cmd := exec.Command("staticcheck", args...) - cmd.Dir = projectRoot() + cmd.Dir = protest.ProjectRoot() cmd.Env = append(os.Environ(), "GOOS=linux", "GOARCH=amd64") out, _ := cmd.CombinedOutput() checkAutogenDoc(t, "_scripts/staticcheck-out.txt", fmt.Sprintf("staticcheck %s > _scripts/staticcheck-out.txt", strings.Join(args, " ")), out) @@ -1435,21 +1408,14 @@ func TestUnixDomainSocket(t *testing.T) { var err error - cmd := exec.Command("go", "run", "_scripts/make.go", "build") - cmd.Dir = projectRoot() - out, err := cmd.CombinedOutput() - if err != nil { - t.Fatalf("makefile error: %v\noutput %s\n", err, string(out)) - } - - dlvbin := filepath.Join(cmd.Dir, "dlv") + dlvbin := protest.GetDlvBinary(t) defer os.Remove(dlvbin) fixtures := protest.FindFixturesDir() buildtestdir := filepath.Join(fixtures, "buildtest") - cmd = exec.Command(dlvbin, "debug", "--headless=true", "--listen=unix:"+listenPath, "--api-version=2", "--backend="+testBackend, "--log", "--log-output=debugger,rpc") + cmd := exec.Command(dlvbin, "debug", "--headless=true", "--listen=unix:"+listenPath, "--api-version=2", "--backend="+testBackend, "--log", "--log-output=debugger,rpc") cmd.Dir = buildtestdir stderr, err := cmd.StderrPipe() assertNoError(err, t, "stderr pipe") diff --git a/pkg/proc/bininfo.go b/pkg/proc/bininfo.go index 2de59c6591..9f9e63bc84 100644 --- a/pkg/proc/bininfo.go +++ b/pkg/proc/bininfo.go @@ -652,6 +652,13 @@ func (fn *Function) privateRuntime() bool { return len(name) > n && name[:n] == "runtime." && !('A' <= name[n] && name[n] <= 'Z') } +func (fn *Function) CompileUnitName() string { + if fn.cu == nil { + return "" + } + return fn.cu.name +} + func rangeParentName(fnname string) int { const rangeSuffix = "-range" ridx := strings.Index(fnname, rangeSuffix) diff --git a/pkg/proc/test/support.go b/pkg/proc/test/support.go index f1d2ae64a5..331b802a8a 100644 --- a/pkg/proc/test/support.go +++ b/pkg/proc/test/support.go @@ -389,3 +389,37 @@ func RegabiSupported() bool { return false } } + +func ProjectRoot() string { + wd, err := os.Getwd() + if err != nil { + panic(err) + } + + gopaths := strings.FieldsFunc(os.Getenv("GOPATH"), func(r rune) bool { return r == os.PathListSeparator }) + for _, curpath := range gopaths { + // Detects "gopath mode" when GOPATH contains several paths ex. "d:\\dir\\gopath;f:\\dir\\gopath2" + if strings.Contains(wd, curpath) { + return filepath.Join(curpath, "src", "github.com", "go-delve", "delve") + } + } + val, err := exec.Command("go", "list", "-mod=", "-m", "-f", "{{ .Dir }}").Output() + if err != nil { + panic(err) // the Go tool was tested to work earlier + } + return strings.TrimSuffix(string(val), "\n") +} + +func GetDlvBinary(t *testing.T) string { + cmd := exec.Command("go", "run", "_scripts/make.go", "build") + cmd.Dir = ProjectRoot() + out, err := cmd.CombinedOutput() + if err != nil { + t.Fatalf("makefile error: %v\noutput %s\n", err, string(out)) + } + + if runtime.GOOS == "windows" { + return filepath.Join(cmd.Dir, "dlv.exe") + } + return filepath.Join(cmd.Dir, "dlv") +} diff --git a/pkg/terminal/command.go b/pkg/terminal/command.go index d90e465737..87b90e5c10 100644 --- a/pkg/terminal/command.go +++ b/pkg/terminal/command.go @@ -555,10 +555,13 @@ Changes the value of a configuration parameter. config substitute-path config substitute-path config substitute-path -clear + config substitute-path -guess Adds or removes a path substitution rule, if -clear is used all substitute-path rules are removed. Without arguments shows the current list of substitute-path rules. +The -guess option causes Delve to try to guess your substitute-path +configuration automatically. See also Documentation/cli/substitutepath.md for how the rules are applied. config alias diff --git a/pkg/terminal/config.go b/pkg/terminal/config.go index 096014da3f..cf13d620b4 100644 --- a/pkg/terminal/config.go +++ b/pkg/terminal/config.go @@ -73,6 +73,17 @@ func configureSetSubstitutePath(t *Term, rest string) error { t.conf.SubstitutePath = t.conf.SubstitutePath[:0] return nil } + if strings.TrimSpace(rest) == "-guess" { + rules, err := t.client.GuessSubstitutePath() + if err != nil { + return err + } + t.conf.SubstitutePath = t.conf.SubstitutePath[:0] + for _, rule := range rules { + t.conf.SubstitutePath = append(t.conf.SubstitutePath, config.SubstitutePathRule{From: rule[0], To: rule[1]}) + } + return nil + } argv := config.SplitQuotedFields(rest, '"') if len(argv) == 2 && argv[0] == "-clear" { argv = argv[1:] diff --git a/pkg/terminal/starbind/starlark_mapping.go b/pkg/terminal/starbind/starlark_mapping.go index 3add7e377f..31a833e3af 100644 --- a/pkg/terminal/starbind/starlark_mapping.go +++ b/pkg/terminal/starbind/starlark_mapping.go @@ -956,6 +956,37 @@ func (env *Env) starlarkPredeclare() (starlark.StringDict, map[string]string) { return env.interfaceToStarlarkValue(rpcRet), nil }) doc["get_thread"] = "builtin get_thread(Id)\n\nget_thread gets a thread by its ID." + r["guess_substitute_path"] = starlark.NewBuiltin("guess_substitute_path", func(thread *starlark.Thread, _ *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) { + if err := isCancelled(thread); err != nil { + return starlark.None, decorateError(thread, err) + } + var rpcArgs rpc2.GuessSubstitutePathIn + var rpcRet rpc2.GuessSubstitutePathOut + if len(args) > 0 && args[0] != starlark.None { + err := unmarshalStarlarkValue(args[0], &rpcArgs.ModuleDirectories, "ModuleDirectories") + if err != nil { + return starlark.None, decorateError(thread, err) + } + } + for _, kv := range kwargs { + var err error + switch kv[0].(starlark.String) { + case "ModuleDirectories": + err = unmarshalStarlarkValue(kv[1], &rpcArgs.ModuleDirectories, "ModuleDirectories") + default: + err = fmt.Errorf("unknown argument %q", kv[0]) + } + if err != nil { + return starlark.None, decorateError(thread, err) + } + } + err := env.ctx.Client().CallAPI("GuessSubstitutePath", &rpcArgs, &rpcRet) + if err != nil { + return starlark.None, err + } + return env.interfaceToStarlarkValue(rpcRet), nil + }) + doc["guess_substitute_path"] = "builtin guess_substitute_path(ModuleDirectories)" r["is_multiclient"] = starlark.NewBuiltin("is_multiclient", func(thread *starlark.Thread, _ *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) { if err := isCancelled(thread); err != nil { return starlark.None, decorateError(thread, err) diff --git a/service/client.go b/service/client.go index baf881ef97..b667e6ae72 100644 --- a/service/client.go +++ b/service/client.go @@ -205,6 +205,9 @@ type Client interface { // GetDebugInfoDirectories returns the list of directories used to search for debug symbols GetDebugInfoDirectories() ([]string, error) + // GuessSubstitutePath tries to guess a substitute-path configuration for the client + GuessSubstitutePath() ([][2]string, error) + // CallAPI allows calling an arbitrary rpc method (used by starlark bindings) CallAPI(method string, args, reply interface{}) error } diff --git a/service/dap/server.go b/service/dap/server.go index 3b6cdaf1fd..c4a87eca4f 100644 --- a/service/dap/server.go +++ b/service/dap/server.go @@ -1975,6 +1975,18 @@ func (s *Session) onAttachRequest(request *dap.AttachRequest) { s.setLaunchAttachArgs(args.LaunchAttachCommonConfig) + if len(args.LaunchAttachCommonConfig.SubstitutePath) == 0 && len(args.ClientMod2Dir) > 0 && s.debugger != nil { + server2Client := s.debugger.GuessSubstitutePath(args.ClientMod2Dir) + clientToServer := make([][2]string, 0, len(server2Client)) + serverToClient := make([][2]string, 0, len(server2Client)) + for serverDir, clientDir := range server2Client { + serverToClient = append(serverToClient, [2]string{serverDir, clientDir}) + clientToServer = append(clientToServer, [2]string{clientDir, serverDir}) + } + s.args.substitutePathClientToServer = clientToServer + s.args.substitutePathServerToClient = serverToClient + } + // Notify the client that the debugger is ready to start accepting // configuration requests for setting breakpoints, etc. The client // will end the configuration sequence with 'configurationDone'. diff --git a/service/dap/types.go b/service/dap/types.go index 59f06065ab..c924126e52 100644 --- a/service/dap/types.go +++ b/service/dap/types.go @@ -252,6 +252,10 @@ type AttachConfig struct { // Wait for a process with a name beginning with this prefix. AttachWaitFor string `json:"waitFor,omitempty"` + // ClientMod2Dir is a map from module paths to client directories, used to + // automatically guess SubstitutePath if it is not specified explicitly. + ClientMod2Dir map[string]string + LaunchAttachCommonConfig } diff --git a/service/debugger/debugger.go b/service/debugger/debugger.go index a638793662..4ee4b489e5 100644 --- a/service/debugger/debugger.go +++ b/service/debugger/debugger.go @@ -12,6 +12,7 @@ import ( "io" "os" "os/exec" + "path" "path/filepath" "regexp" "runtime" @@ -2565,3 +2566,115 @@ func (d *Debugger) maybePrintUnattendedBreakpointWarning(stopReason proc.StopRea } api.PrintStack(formatPathFunc, os.Stderr, apiFrames, "", false, api.StackTraceColors{}, includeFunc) } + +// GuessSubstitutePath returns a substitute-path configuration that maps +// server paths to client paths by examining the executable file and a map +// of module paths to client directories (clientMod2Dir) passed as input. +func (d *Debugger) GuessSubstitutePath(clientMod2Dir map[string]string) map[string]string { + serverMod2Dir := map[string]string{} + serverMod2DirCandidate := map[string]map[string]int{} + + for mod := range clientMod2Dir { + serverMod2DirCandidate[mod] = make(map[string]int) + } + + const minEvidence = 10 + const decisionThreshold = 0.8 + + totCandidates := func(mod string) int { + r := 0 + for _, cnt := range serverMod2DirCandidate[mod] { + r += cnt + } + return r + } + + bestCandidate := func(mod string) string { + best := "" + for dir, cnt := range serverMod2DirCandidate[mod] { + if cnt > serverMod2DirCandidate[mod][best] { + best = dir + } + } + return best + } + + slashes := func(s string) int { + r := 0 + for _, ch := range s { + if ch == '/' { + r++ + } + } + return r + } + + logger := logflags.DebuggerLogger() + + tgt := proc.ValidTargets{Group: d.target} + for tgt.Next() { + bi := tgt.BinInfo() + for i := range bi.Functions { + fn := &bi.Functions[i] + fnpkg := fn.PackageName() + if fn.CompileUnitName() != "" && strings.ReplaceAll(fn.CompileUnitName(), "\\", "/") != fnpkg { + // inlined + continue + } + fnmod := "" + for mod := range clientMod2Dir { + if strings.HasPrefix(fnpkg, mod) { + fnmod = mod + break + } + } + if fnmod == "" { + // not in any module we are interested in + continue + } + if serverMod2Dir[fnmod] != "" { + // already decided + continue + } + + elems := slashes(fnpkg[len(fnmod):]) + + file, _ := bi.EntryLineForFunc(fn) + if file == "" || file == "" { + continue + } + logger.Debugf("considering %s pkg:%s compile unit:%s file:%s", fn.Name, fnpkg, fn.CompileUnitName(), file) + dir := path.Dir(file) // note: paths are normalized to always use '/' as a separator by pkg/dwarf/line + if slashes(dir) < elems { + continue + } + for i := 0; i < elems; i++ { + dir = path.Dir(dir) + } + + serverMod2DirCandidate[fnmod][dir]++ + + n := totCandidates(fnmod) + best := bestCandidate(fnmod) + if n > minEvidence && float64(serverMod2DirCandidate[fnmod][best])/float64(n) > decisionThreshold { + serverMod2Dir[fnmod] = best + } + } + } + + for mod := range clientMod2Dir { + if serverMod2Dir[mod] == "" { + serverMod2Dir[mod] = bestCandidate(mod) + } + } + + server2Client := make(map[string]string) + + for mod, clientDir := range clientMod2Dir { + if serverMod2Dir[mod] != "" { + server2Client[serverMod2Dir[mod]] = clientDir + } + } + + return server2Client +} diff --git a/service/rpc2/client.go b/service/rpc2/client.go index 3b0da21ebc..1826ddc73c 100644 --- a/service/rpc2/client.go +++ b/service/rpc2/client.go @@ -1,11 +1,16 @@ package rpc2 import ( + "bytes" + "encoding/json" "fmt" "log" "net" "net/rpc" "net/rpc/jsonrpc" + "os/exec" + "path/filepath" + "strings" "time" "github.com/go-delve/delve/service" @@ -578,6 +583,68 @@ func (c *RPCClient) GetDebugInfoDirectories() ([]string, error) { return out.List, err } +type goListEntry struct { + Dir string + ImportPath string + Module *goListModule +} + +type goListModule struct { + Path string +} + +// MakeGuessSusbtitutePathIn returns a mapping from modules to client +// directories using "go list". +func MakeGuessSusbtitutePathIn() (*GuessSubstitutePathIn, error) { + cmd := exec.Command("go", "list", "--json", "all") + buf, err := cmd.Output() + if err != nil { + return nil, err + } + mod2dir := make(map[string]string) + d := json.NewDecoder(bytes.NewReader(buf)) + for d.More() { + var e goListEntry + err := d.Decode(&e) + if err != nil { + return nil, err + } + if e.Module == nil { + continue + } + if !strings.HasPrefix(e.ImportPath, e.Module.Path) { + continue + } + pkgWithoutModule := e.ImportPath[len(e.Module.Path):] + elems := 0 + for _, c := range pkgWithoutModule { + if c == '/' { + elems++ + } + } + dir := e.Dir + for i := 0; i < elems; i++ { + dir = filepath.Dir(dir) + } + if mod2dir[e.Module.Path] != "" && mod2dir[e.Module.Path] != dir { + return nil, fmt.Errorf("could not determine path for module %s (got %q and %q)", e.Module.Path, mod2dir[e.Module.Path], dir) + } + mod2dir[e.Module.Path] = dir + } + return &GuessSubstitutePathIn{ModuleDirectories: mod2dir}, nil +} + +func (c *RPCClient) GuessSubstitutePath() ([][2]string, error) { + in, err := MakeGuessSusbtitutePathIn() + if err != nil { + return nil, err + } + + out := &GuessSubstitutePathOut{} + err = c.call("GuessSubstitutePath", *in, out) + return out.List, err +} + func (c *RPCClient) call(method string, args, reply interface{}) error { return c.client.Call("RPCServer."+method, args, reply) } diff --git a/service/rpc2/guess_substitute_path_test.go b/service/rpc2/guess_substitute_path_test.go new file mode 100644 index 0000000000..1e7ae77222 --- /dev/null +++ b/service/rpc2/guess_substitute_path_test.go @@ -0,0 +1,17 @@ +package rpc2 + +import ( + "runtime" + "testing" +) + +func TestMakeGuessSusbtitutePathIn(t *testing.T) { + if runtime.GOARCH == "ppc64le" { + t.Setenv("GOFLAGS", "-tags=exp.linuxppc64le") + } + gsp, err := MakeGuessSusbtitutePathIn() + if err != nil { + t.Fatal(err) + } + t.Logf("%#v", gsp) +} diff --git a/service/rpc2/server.go b/service/rpc2/server.go index 9c92ccade1..73c3ec4338 100644 --- a/service/rpc2/server.go +++ b/service/rpc2/server.go @@ -1144,3 +1144,19 @@ func (s *RPCServer) DebugInfoDirectories(arg DebugInfoDirectoriesIn, out *DebugI out.List = s.debugger.DebugInfoDirectories() return nil } + +type GuessSubstitutePathIn struct { + ModuleDirectories map[string]string +} + +type GuessSubstitutePathOut struct { + List [][2]string +} + +func (s *RPCServer) GuessSubstitutePath(arg GuessSubstitutePathIn, out *GuessSubstitutePathOut) error { + m := s.debugger.GuessSubstitutePath(arg.ModuleDirectories) + for k, v := range m { + out.List = append(out.List, [2]string{k, v}) + } + return nil +} diff --git a/service/test/integration2_test.go b/service/test/integration2_test.go index 6fc5f642d9..7daf1947f1 100644 --- a/service/test/integration2_test.go +++ b/service/test/integration2_test.go @@ -3167,3 +3167,132 @@ func TestBreakpointVariablesWithoutG(t *testing.T) { assertNoError(state.Err, t, "Continue()") }) } + +func TestGuessSubstitutePath(t *testing.T) { + t.Setenv("NOCERT", "1") + slashnorm := func(s string) string { + if runtime.GOOS != "windows" { + return s + } + return strings.ReplaceAll(s, "\\", "/") + } + + guess := func(t *testing.T, goflags string) [][2]string { + oldgoflags := os.Getenv("GOFLAGS") + os.Setenv("GOFLAGS", goflags) + defer os.Setenv("GOFLAGS", oldgoflags) + + dlvbin := protest.GetDlvBinary(t) + defer os.Remove(dlvbin) + + listener, clientConn := service.ListenerPipe() + defer listener.Close() + server := rpccommon.NewServer(&service.Config{ + Listener: listener, + ProcessArgs: []string{dlvbin, "help"}, + Debugger: debugger.Config{ + Backend: testBackend, + CheckGoVersion: true, + BuildFlags: "", // build flags can be an empty string here because the only test that uses it, does not set special flags. + ExecuteKind: debugger.ExecutingExistingFile, + }, + }) + if err := server.Run(); err != nil { + t.Fatal(err) + } + + client := rpc2.NewClientFromConn(clientConn) + defer client.Detach(true) + + if runtime.GOARCH == "ppc64le" { + os.Setenv("GOFLAGS", "-tags=exp.linuxppc64le") + } + + gsp, err := client.GuessSubstitutePath() + assertNoError(err, t, "GuessSubstitutePath") + return gsp + } + + delvePath := protest.ProjectRoot() + var nmods int = -1 + + t.Run("Normal", func(t *testing.T) { + gsp := guess(t, "") + t.Logf("Normal build: %d", len(gsp)) + if len(gsp) == 0 { + t.Fatalf("not enough modules") + } + found := false + for _, e := range gsp { + t.Logf("\t%s -> %s", e[0], e[1]) + if e[0] != slashnorm(e[1]) { + t.Fatalf("mismatch %q %q", e[0], e[1]) + } + if e[1] == delvePath { + found = true + } + } + nmods = len(gsp) + if !found { + t.Fatalf("could not find main module path %q", delvePath) + } + + if os.Getenv("CI") == "true" { + return + } + }) + + t.Run("Modules", func(t *testing.T) { + gsp := guess(t, "-mod=mod") + t.Logf("Modules build: %d", len(gsp)) + if len(gsp) != nmods && nmods != -1 { + t.Fatalf("not enough modules") + } + found := false + for _, e := range gsp { + t.Logf("\t%s -> %s", e[0], e[1]) + if e[0] == slashnorm(delvePath) && e[1] == delvePath { + found = true + } + } + if !found { + t.Fatalf("could not find main module path %q", delvePath) + } + }) + + t.Run("Trimpath", func(t *testing.T) { + gsp := guess(t, "-trimpath") + t.Logf("Trimpath build: %d", len(gsp)) + if len(gsp) != nmods && nmods != -1 { + t.Fatalf("not enough modules") + } + found := false + for _, e := range gsp { + t.Logf("\t%s -> %s", e[0], e[1]) + if e[0] == "github.com/go-delve/delve" && e[1] == delvePath { + found = true + } + } + if !found { + t.Fatalf("could not find main module path %q", delvePath) + } + }) + + t.Run("ModulesTrimpath", func(t *testing.T) { + gsp := guess(t, "-trimpath -mod=mod") + t.Logf("Modules+Trimpath build: %d", len(gsp)) + if len(gsp) != nmods && nmods != -1 { + t.Fatalf("not enough modules") + } + found := false + for _, e := range gsp { + t.Logf("\t%s -> %s", e[0], e[1]) + if e[0] == "github.com/go-delve/delve" && e[1] == delvePath { + found = true + } + } + if !found { + t.Fatalf("could not find main module path %q", delvePath) + } + }) +}