service,terminal,cmd/dlv: automatically guessing substitute-path config (#3781)

Add command, API calls and launch.json option to automatically guess
substitute-path configuration.
This commit is contained in:
Alessandro Arzilli 2024-10-31 18:19:08 +01:00 committed by GitHub
parent ac14553fda
commit 822014b8e8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
20 changed files with 650 additions and 95 deletions

@ -256,10 +256,13 @@ Changes the value of a configuration parameter.
config substitute-path <from> <to>
config substitute-path <from>
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 <command> <alias>

@ -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(Args) | 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)

@ -24,8 +24,10 @@ func main() {
cmdnames := []string{}
for _, subcmd := range root.Commands() {
if !subcmd.Hidden {
cmdnames = append(cmdnames, subcmd.Name())
}
}
helphelpers.Prepare(root)
doc.GenMarkdownTree(root, usageDir)
root = nil

@ -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
@ -326,7 +329,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)}

@ -513,6 +513,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)

@ -31,11 +31,6 @@ import (
)
var testBackend string
var ldFlags string
func init() {
ldFlags = os.Getenv("CGO_LDFLAGS")
}
func TestMain(m *testing.M) {
flag.StringVar(&testBackend, "backend", "", "selects backend")
@ -61,30 +56,10 @@ 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"
dlvbin := getDlvBin(t)
dlvbin := protest.GetDlvBinary(t)
defer os.Remove(dlvbin)
fixtures := protest.FindFixturesDir()
@ -194,46 +169,10 @@ func testOutput(t *testing.T, dlvbin, output string, delveCmds []string) (stdout
return
}
func getDlvBin(t *testing.T) string {
// In case this was set in the environment
// from getDlvBinEBPF lets clear it here, so
// we can ensure we don't get build errors
// depending on the test ordering.
t.Setenv("CGO_LDFLAGS", ldFlags)
var tags string
if runtime.GOOS == "windows" && runtime.GOARCH == "arm64" {
tags = "-tags=exp.winarm64"
}
if runtime.GOOS == "linux" && runtime.GOARCH == "ppc64le" {
tags = "-tags=exp.linuxppc64le"
}
if runtime.GOOS == "linux" && runtime.GOARCH == "riscv64" {
tags = "-tags=exp.linuxriscv64"
}
return getDlvBinInternal(t, tags)
}
func getDlvBinEBPF(t *testing.T) string {
return getDlvBinInternal(t, "-tags", "ebpf")
}
func getDlvBinInternal(t *testing.T, goflags ...string) string {
dlvbin := filepath.Join(t.TempDir(), "dlv.exe")
args := append([]string{"build", "-o", dlvbin}, goflags...)
args = append(args, "github.com/go-delve/delve/cmd/dlv")
out, err := exec.Command("go", args...).CombinedOutput()
if err != nil {
t.Fatalf("go build -o %v github.com/go-delve/delve/cmd/dlv: %v\n%s", dlvbin, err, string(out))
}
return dlvbin
}
// TestOutput verifies that the debug executable is created in the correct path
// and removed after exit.
func TestOutput(t *testing.T) {
dlvbin := getDlvBin(t)
dlvbin := protest.GetDlvBinary(t)
for _, output := range []string{"__debug_bin", "myownname", filepath.Join(t.TempDir(), "absolute.path")} {
testOutput(t, dlvbin, output, []string{"exit"})
@ -252,7 +191,7 @@ func TestUnattendedBreakpoint(t *testing.T) {
const listenAddr = "127.0.0.1:40573"
fixturePath := filepath.Join(protest.FindFixturesDir(), "panic.go")
cmd := exec.Command(getDlvBin(t), "debug", "--continue", "--headless", "--accept-multiclient", "--listen", listenAddr, fixturePath)
cmd := exec.Command(protest.GetDlvBinary(t), "debug", "--continue", "--headless", "--accept-multiclient", "--listen", listenAddr, fixturePath)
stderr, err := cmd.StderrPipe()
assertNoError(err, t, "stdout pipe")
defer stderr.Close()
@ -279,7 +218,7 @@ func TestUnattendedBreakpoint(t *testing.T) {
func TestContinue(t *testing.T) {
const listenAddr = "127.0.0.1:40573"
dlvbin := getDlvBin(t)
dlvbin := protest.GetDlvBinary(t)
buildtestdir := filepath.Join(protest.FindFixturesDir(), "buildtest")
cmd := exec.Command(dlvbin, "debug", "--headless", "--continue", "--accept-multiclient", "--listen", listenAddr)
@ -311,7 +250,7 @@ func TestContinue(t *testing.T) {
func TestRedirect(t *testing.T) {
const listenAddr = "127.0.0.1:40573"
dlvbin := getDlvBin(t)
dlvbin := protest.GetDlvBinary(t)
catfixture := filepath.Join(protest.FindFixturesDir(), "cat.go")
cmd := exec.Command(dlvbin, "debug", "--headless", "--continue", "--accept-multiclient", "--listen", listenAddr, "-r", catfixture, catfixture)
@ -339,7 +278,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'})
@ -377,7 +316,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)
@ -410,7 +349,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)
@ -424,7 +363,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))
@ -443,7 +382,7 @@ func TestGeneratedDoc(t *testing.T) {
}
func TestExitInInit(t *testing.T) {
dlvbin := getDlvBin(t)
dlvbin := protest.GetDlvBinary(t)
buildtestdir := filepath.Join(protest.FindFixturesDir(), "buildtest")
exitInit := filepath.Join(protest.FindFixturesDir(), "exit.init")
@ -643,7 +582,7 @@ func TestTypecheckRPC(t *testing.T) {
func TestDAPCmd(t *testing.T) {
const listenAddr = "127.0.0.1:40575"
dlvbin := getDlvBin(t)
dlvbin := protest.GetDlvBinary(t)
cmd := exec.Command(dlvbin, "dap", "--log-output=dap", "--log", "--listen", listenAddr)
stdout, err := cmd.StdoutPipe()
@ -706,7 +645,7 @@ func newDAPRemoteClient(t *testing.T, addr string, isDlvAttach bool, isMulti boo
func TestRemoteDAPClient(t *testing.T) {
const listenAddr = "127.0.0.1:40576"
dlvbin := getDlvBin(t)
dlvbin := protest.GetDlvBinary(t)
buildtestdir := filepath.Join(protest.FindFixturesDir(), "buildtest")
cmd := exec.Command(dlvbin, "debug", "--headless", "--log-output=dap", "--log", "--listen", listenAddr)
@ -759,7 +698,7 @@ func closeDAPRemoteMultiClient(t *testing.T, c *daptest.Client, expectStatus str
func TestRemoteDAPClientMulti(t *testing.T) {
const listenAddr = "127.0.0.1:40577"
dlvbin := getDlvBin(t)
dlvbin := protest.GetDlvBinary(t)
buildtestdir := filepath.Join(protest.FindFixturesDir(), "buildtest")
cmd := exec.Command(dlvbin, "debug", "--headless", "--accept-multiclient", "--log-output=debugger", "--log", "--listen", listenAddr)
@ -826,7 +765,7 @@ func TestRemoteDAPClientMulti(t *testing.T) {
func TestRemoteDAPClientAfterContinue(t *testing.T) {
const listenAddr = "127.0.0.1:40578"
dlvbin := getDlvBin(t)
dlvbin := protest.GetDlvBinary(t)
fixture := protest.BuildFixture("loopprog", 0)
cmd := exec.Command(dlvbin, "exec", fixture.Path, "--headless", "--continue", "--accept-multiclient", "--log-output=debugger,dap", "--log", "--listen", listenAddr)
@ -887,7 +826,7 @@ func TestDAPCmdWithClient(t *testing.T) {
}
defer listener.Close()
dlvbin := getDlvBin(t)
dlvbin := protest.GetDlvBinary(t)
cmd := exec.Command(dlvbin, "dap", "--log-output=dap", "--log", "--client-addr", listener.Addr().String())
buf := &bytes.Buffer{}
@ -934,7 +873,7 @@ func TestDAPCmdWithUnixClient(t *testing.T) {
}
defer listener.Close()
dlvbin := getDlvBin(t)
dlvbin := protest.GetDlvBinary(t)
cmd := exec.Command(dlvbin, "dap", "--log-output=dap", "--log", "--client-addr=unix:"+listener.Addr().String())
buf := &bytes.Buffer{}
@ -968,7 +907,7 @@ func TestDAPCmdWithUnixClient(t *testing.T) {
}
func TestTrace(t *testing.T) {
dlvbin := getDlvBin(t)
dlvbin := protest.GetDlvBinary(t)
expected := []byte("> goroutine(1): main.foo(99, 9801)\n>> goroutine(1): main.foo => (9900)\n")
@ -992,7 +931,7 @@ func TestTrace(t *testing.T) {
}
func TestTrace2(t *testing.T) {
dlvbin := getDlvBin(t)
dlvbin := protest.GetDlvBinary(t)
expected := []byte("> goroutine(1): main.callme(2)\n>> goroutine(1): main.callme => (4)\n")
@ -1016,7 +955,7 @@ func TestTrace2(t *testing.T) {
}
func TestTraceDirRecursion(t *testing.T) {
dlvbin := getDlvBin(t)
dlvbin := protest.GetDlvBinary(t)
expected := []byte("> goroutine(1):frame(1) main.A(5, 5)\n > goroutine(1):frame(2) main.A(4, 4)\n > goroutine(1):frame(3) main.A(3, 3)\n > goroutine(1):frame(4) main.A(2, 2)\n > goroutine(1):frame(5) main.A(1, 1)\n >> goroutine(1):frame(5) main.A => (1)\n >> goroutine(1):frame(4) main.A => (2)\n >> goroutine(1):frame(3) main.A => (6)\n >> goroutine(1):frame(2) main.A => (24)\n>> goroutine(1):frame(1) main.A => (120)\n")
@ -1049,7 +988,7 @@ func TestTraceDirRecursion(t *testing.T) {
}
func TestTraceMultipleGoroutines(t *testing.T) {
dlvbin := getDlvBin(t)
dlvbin := protest.GetDlvBinary(t)
// TODO(derekparker) this test has to be a bit vague to avoid flakiness.
// I think a future improvement could be to use regexp captures to match the
@ -1088,7 +1027,7 @@ func TestTracePid(t *testing.T) {
}
}
dlvbin := getDlvBin(t)
dlvbin := protest.GetDlvBinary(t)
expected := []byte("goroutine(1): main.A()\n>> goroutine(1): main.A => ()\n")
@ -1121,7 +1060,7 @@ func TestTracePid(t *testing.T) {
}
func TestTraceBreakpointExists(t *testing.T) {
dlvbin := getDlvBin(t)
dlvbin := protest.GetDlvBinary(t)
fixtures := protest.FindFixturesDir()
// We always set breakpoints on some runtime functions at startup, so this would return with
@ -1147,7 +1086,7 @@ func TestTraceBreakpointExists(t *testing.T) {
}
func TestTracePrintStack(t *testing.T) {
dlvbin := getDlvBin(t)
dlvbin := protest.GetDlvBinary(t)
fixtures := protest.FindFixturesDir()
cmd := exec.Command(dlvbin, "trace", "--output", filepath.Join(t.TempDir(), "__debug"), "--stack", "2", filepath.Join(fixtures, "issue573.go"), "foo")
@ -1186,7 +1125,7 @@ func TestTraceEBPF(t *testing.T) {
t.Skip("test must be run as root")
}
dlvbin := getDlvBinEBPF(t)
dlvbin := protest.GetDlvBinaryEBPF(t)
expected := []byte("> (1) main.foo(99, 9801)\n=> \"9900\"")
@ -1225,7 +1164,7 @@ func TestTraceEBPF2(t *testing.T) {
t.Skip("test must be run as root")
}
dlvbin := getDlvBinEBPF(t)
dlvbin := protest.GetDlvBinaryEBPF(t)
expected := []byte(`> (1) main.callme(10)
> (1) main.callme(9)
@ -1285,7 +1224,7 @@ func TestTraceEBPF3(t *testing.T) {
t.Skip("test must be run as root")
}
dlvbin := getDlvBinEBPF(t)
dlvbin := protest.GetDlvBinaryEBPF(t)
expected := []byte(`> (1) main.tracedFunction(0)
> (1) main.tracedFunction(1)
@ -1333,7 +1272,7 @@ func TestTraceEBPF4(t *testing.T) {
t.Skip("test must be run as root")
}
dlvbin := getDlvBinEBPF(t)
dlvbin := protest.GetDlvBinaryEBPF(t)
expected := []byte(`> (1) main.tracedFunction(0, true, 97)
> (1) main.tracedFunction(1, false, 98)
@ -1364,7 +1303,7 @@ func TestTraceEBPF4(t *testing.T) {
}
func TestDlvTestChdir(t *testing.T) {
dlvbin := getDlvBin(t)
dlvbin := protest.GetDlvBinary(t)
fixtures := protest.FindFixturesDir()
@ -1396,7 +1335,7 @@ func TestDlvTestChdir(t *testing.T) {
}
func TestVersion(t *testing.T) {
dlvbin := getDlvBin(t)
dlvbin := protest.GetDlvBinary(t)
got, err := exec.Command(dlvbin, "version", "-v").CombinedOutput()
if err != nil {
@ -1425,7 +1364,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)
@ -1434,7 +1373,7 @@ func TestStaticcheck(t *testing.T) {
func TestDefaultBinary(t *testing.T) {
// Check that when delve is run twice in the same directory simultaneously
// it will pick different default output binary paths.
dlvbin := getDlvBin(t)
dlvbin := protest.GetDlvBinary(t)
fixture := filepath.Join(protest.FindFixturesDir(), "testargs.go")
startOne := func() (io.WriteCloser, func() error, *bytes.Buffer) {
@ -1475,7 +1414,9 @@ func TestUnixDomainSocket(t *testing.T) {
listenPath := filepath.Join(tmpdir, "delve_test")
dlvbin := getDlvBin(t)
var err error
dlvbin := protest.GetDlvBinary(t)
defer os.Remove(dlvbin)
fixtures := protest.FindFixturesDir()

@ -665,6 +665,13 @@ func (fn *Function) privateRuntime() bool {
return len(name) > n && name[:n] == "runtime." && !('A' <= name[n] && name[n] <= 'Z')
}
func (fn *Function) CompilationUnitName() string {
if fn.cu == nil {
return ""
}
return fn.cu.name
}
func rangeParentName(fnname string) int {
const rangeSuffix = "-range"
ridx := strings.Index(fnname, rangeSuffix)

@ -22,6 +22,12 @@ var EnableRace = flag.Bool("racetarget", false, "Enables race detector on inferi
var runningWithFixtures bool
var ldFlags string
func init() {
ldFlags = os.Getenv("CGO_LDFLAGS")
}
// Fixture is a test binary.
type Fixture struct {
// Name is the short name of the fixture.
@ -392,3 +398,62 @@ 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 {
// In case this was set in the environment
// from getDlvBinEBPF lets clear it here, so
// we can ensure we don't get build errors
// depending on the test ordering.
t.Setenv("CGO_LDFLAGS", ldFlags)
var tags []string
if runtime.GOOS == "windows" && runtime.GOARCH == "arm64" {
tags = []string{"-tags=exp.winarm64"}
}
if runtime.GOOS == "linux" && runtime.GOARCH == "ppc64le" {
tags = []string{"-tags=exp.linuxppc64le"}
}
if runtime.GOOS == "linux" && runtime.GOARCH == "riscv64" {
tags = []string{"-tags=exp.linuxriscv64"}
}
return getDlvBinInternal(t, tags...)
}
func GetDlvBinaryEBPF(t *testing.T) string {
return getDlvBinInternal(t, "-tags", "ebpf")
}
func getDlvBinInternal(t *testing.T, goflags ...string) string {
dlvbin := filepath.Join(t.TempDir(), "dlv.exe")
args := append([]string{"build", "-o", dlvbin}, goflags...)
args = append(args, "github.com/go-delve/delve/cmd/dlv")
wd, _ := os.Getwd()
fmt.Printf("at %s %s\n", wd, goflags)
out, err := exec.Command("go", args...).CombinedOutput()
if err != nil {
t.Fatalf("go build -o %v github.com/go-delve/delve/cmd/dlv: %v\n%s", dlvbin, err, string(out))
}
return dlvbin
}

@ -555,10 +555,13 @@ Changes the value of a configuration parameter.
config substitute-path <from> <to>
config substitute-path <from>
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 <command> <alias>

@ -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]})
}
rest = "" // print the result
}
argv := config.SplitQuotedFields(rest, '"')
if len(argv) == 2 && argv[0] == "-clear" {
argv = argv[1:]

@ -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.Args, "Args")
if err != nil {
return starlark.None, decorateError(thread, err)
}
}
for _, kv := range kwargs {
var err error
switch kv[0].(starlark.String) {
case "Args":
err = unmarshalStarlarkValue(kv[1], &rpcArgs.Args, "Args")
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(Args)"
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)

@ -677,3 +677,10 @@ type Target struct {
CmdLine string
CurrentThread *Thread
}
// GuessSubstitutePathIn are the input parameters used to guess a substitute-path configuration automatically.
type GuessSubstitutePathIn struct {
ImportPathOfMainPackage string
ClientGOROOT string
ClientModuleDirectories map[string]string
}

@ -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
}

@ -1975,6 +1975,18 @@ func (s *Session) onAttachRequest(request *dap.AttachRequest) {
s.setLaunchAttachArgs(args.LaunchAttachCommonConfig)
if len(args.LaunchAttachCommonConfig.SubstitutePath) == 0 && args.GuessSubstitutePath != nil && s.debugger != nil {
server2Client := s.debugger.GuessSubstitutePath(args.GuessSubstitutePath)
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'.

@ -4,6 +4,8 @@ import (
"encoding/json"
"errors"
"fmt"
"github.com/go-delve/delve/service/api"
)
// Launch debug sessions support the following modes:
@ -252,6 +254,11 @@ type AttachConfig struct {
// Wait for a process with a name beginning with this prefix.
AttachWaitFor string `json:"waitFor,omitempty"`
// GuessSubstitutePath is used to automatically guess SubstitutePath if it
// is not specified explicitly. It should be copied from the output of
// 'dlv substitute-path-guess-helper'.
GuessSubstitutePath *api.GuessSubstitutePathIn `json:"guessSubstitutePath,omitempty"`
LaunchAttachCommonConfig
}

@ -12,6 +12,7 @@ import (
"io"
"os"
"os/exec"
"path"
"path/filepath"
"regexp"
"runtime"
@ -2551,3 +2552,157 @@ 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(args *api.GuessSubstitutePathIn) map[string]string {
bis := []*proc.BinaryInfo{}
bins := [][]proc.Function{}
tgt := proc.ValidTargets{Group: d.target}
for tgt.Next() {
bi := tgt.BinInfo()
bis = append(bis, bi)
bins = append(bins, bi.Functions)
}
return guessSubstitutePath(args, bins, func(biIdx int, fn *proc.Function) string {
file, _ := bis[biIdx].EntryLineForFunc(fn)
return file
})
}
func guessSubstitutePath(args *api.GuessSubstitutePathIn, bins [][]proc.Function, fileForFunc func(int, *proc.Function) string) map[string]string {
serverMod2Dir := map[string]string{}
serverMod2DirCandidate := map[string]map[string]int{}
pkg2mod := map[string]string{}
for mod := range args.ClientModuleDirectories {
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
}
serverGoroot := ""
logger := logflags.DebuggerLogger()
for binIdx, bin := range bins {
for i := range bin {
fn := &bin[i]
if fn.Name == "runtime.main" && serverGoroot == "" {
file := fileForFunc(binIdx, fn)
serverGoroot = path.Dir(path.Dir(path.Dir(file)))
continue
}
fnpkg := fn.PackageName()
if fn.CompilationUnitName() != "" && strings.ReplaceAll(fn.CompilationUnitName(), "\\", "/") != fnpkg {
// inlined
continue
}
if fnpkg == "main" && binIdx == 0 && args.ImportPathOfMainPackage != "" {
fnpkg = args.ImportPathOfMainPackage
}
fnmod := ""
if mod, ok := pkg2mod[fnpkg]; ok {
fnmod = mod
} else {
for mod := range args.ClientModuleDirectories {
if strings.HasPrefix(fnpkg, mod) {
fnmod = mod
break
}
}
pkg2mod[fnpkg] = fnmod
if fnmod == "" {
logger.Debugf("No module detected for server package %q", fnpkg)
}
}
if fnmod == "" {
// not in any module we are interested in
continue
}
if serverMod2Dir[fnmod] != "" {
// already decided
continue
}
elems := slashes(fnpkg[len(fnmod):])
file := fileForFunc(binIdx, fn)
if file == "" || file == "<autogenerated>" {
continue
}
logger.Debugf("considering %s pkg:%s compile unit:%s file:%s", fn.Name, fnpkg, fn.CompilationUnitName(), 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 args.ClientModuleDirectories {
if serverMod2Dir[mod] == "" {
serverMod2Dir[mod] = bestCandidate(mod)
}
}
server2Client := make(map[string]string)
for mod, clientDir := range args.ClientModuleDirectories {
if serverMod2Dir[mod] != "" {
server2Client[serverMod2Dir[mod]] = clientDir
}
}
if serverGoroot != "" && args.ClientGOROOT != "" {
server2Client[serverGoroot] = args.ClientGOROOT
}
return server2Client
}

@ -1,6 +1,7 @@
package debugger
import (
"flag"
"fmt"
"os"
"path/filepath"
@ -9,10 +10,20 @@ import (
"testing"
"github.com/go-delve/delve/pkg/gobuild"
"github.com/go-delve/delve/pkg/logflags"
"github.com/go-delve/delve/pkg/proc"
protest "github.com/go-delve/delve/pkg/proc/test"
"github.com/go-delve/delve/service/api"
)
func TestMain(m *testing.M) {
var logConf string
flag.StringVar(&logConf, "log", "", "configures logging")
flag.Parse()
logflags.Setup(logConf != "", logConf, "")
os.Exit(protest.RunTestsWithFixtures(m))
}
func TestDebugger_LaunchNoMain(t *testing.T) {
fixturesDir := protest.FindFixturesDir()
nomaindir := filepath.Join(fixturesDir, "nomaindir")
@ -100,3 +111,45 @@ func TestDebugger_LaunchCurrentDir(t *testing.T) {
t.Fatal(err)
}
}
func guessSubstitutePathHelper(t *testing.T, args *api.GuessSubstitutePathIn, fnpaths [][2]string, tgt map[string]string) {
const base = 0x40000
t.Helper()
bins := [][]proc.Function{[]proc.Function{}}
for i, fnpath := range fnpaths {
bins[0] = append(bins[0], proc.Function{Name: fnpath[0], Entry: uint64(base + i)})
}
out := guessSubstitutePath(args, bins, func(_ int, fn *proc.Function) string {
return fnpaths[fn.Entry-base][1]
})
t.Logf("%#v\n", out)
if len(out) != len(tgt) {
t.Errorf("wrong number of entries")
return
}
for k := range out {
if out[k] != tgt[k] {
t.Errorf("mismatch for directory %q", k)
return
}
}
}
func TestGuessSubstitutePathMinimalMain(t *testing.T) {
// When the main module only contains a single package check that its mapping still works
guessSubstitutePathHelper(t,
&api.GuessSubstitutePathIn{
ImportPathOfMainPackage: "github.com/ccampo133/go-docker-alpine-remote-debug",
ClientGOROOT: "/user/go/pkg/mod/golang.org/toolchain@v0.0.1-go1.23.0.linux-amd64",
ClientModuleDirectories: map[string]string{
"github.com/ccampo133/go-docker-alpine-remote-debug": "/user/gohome/go-docker-alpine-remote-debug",
},
},
[][2]string{
{"main.main", "/app/main.go"},
{"main.hello", "/app/main.go"},
{"runtime.main", "/usr/local/go/src/runtime/main.go"}},
map[string]string{
"/usr/local/go": "/user/go/pkg/mod/golang.org/toolchain@v0.0.1-go1.23.0.linux-amd64",
"/app": "/user/gohome/go-docker-alpine-remote-debug"})
}

@ -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,86 @@ func (c *RPCClient) GetDebugInfoDirectories() ([]string, error) {
return out.List, err
}
type goListEntry struct {
Dir string
ImportPath string
Name string
Module *goListModule
}
type goListModule struct {
Path string
}
// MakeGuessSusbtitutePathIn returns a mapping from modules to client
// directories using "go list".
func MakeGuessSusbtitutePathIn() (*api.GuessSubstitutePathIn, error) {
cmd := exec.Command("go", "list", "--json", "all")
buf, err := cmd.Output()
if err != nil {
return nil, err
}
importPathOfMainPackage := ""
importPathOfMainPackageOk := true
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
if e.Name == "main" {
if importPathOfMainPackage != "" && importPathOfMainPackage != e.ImportPath {
importPathOfMainPackageOk = false
}
importPathOfMainPackage = e.ImportPath
}
}
buf, err = exec.Command("go", "env", "GOROOT").Output()
if err != nil {
return nil, err
}
clientGoroot := strings.TrimSpace(string(buf))
if !importPathOfMainPackageOk {
// There were multiple main packages
importPathOfMainPackage = ""
}
return &api.GuessSubstitutePathIn{ClientGOROOT: clientGoroot, ImportPathOfMainPackage: importPathOfMainPackage, ClientModuleDirectories: mod2dir}, nil
}
func (c *RPCClient) GuessSubstitutePath() ([][2]string, error) {
in, err := MakeGuessSusbtitutePathIn()
if err != nil {
return nil, err
}
out := &GuessSubstitutePathOut{}
err = c.call("GuessSubstitutePath", GuessSubstitutePathIn{*in}, out)
return out.List, err
}
func (c *RPCClient) call(method string, args, reply interface{}) error {
return c.client.Call("RPCServer."+method, args, reply)
}

@ -1146,3 +1146,19 @@ func (s *RPCServer) DebugInfoDirectories(arg DebugInfoDirectoriesIn, out *DebugI
out.List = s.debugger.DebugInfoDirectories()
return nil
}
type GuessSubstitutePathIn struct {
Args api.GuessSubstitutePathIn
}
type GuessSubstitutePathOut struct {
List [][2]string
}
func (s *RPCServer) GuessSubstitutePath(arg GuessSubstitutePathIn, out *GuessSubstitutePathOut) error {
m := s.debugger.GuessSubstitutePath(&arg.Args)
for k, v := range m {
out.List = append(out.List, [2]string{k, v})
}
return nil
}

@ -3167,3 +3167,135 @@ 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)
switch runtime.GOARCH {
case "ppc64le":
os.Setenv("GOFLAGS", "-tags=exp.linuxppc64le")
case "riscv64":
os.Setenv("GOFLAGS", "-tags=exp.linuxriscv64")
}
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)
}
})
}