diff --git a/cmd/internal/debug/debug.go b/cmd/internal/debug/debug.go new file mode 100644 index 0000000000..b232f1d18e --- /dev/null +++ b/cmd/internal/debug/debug.go @@ -0,0 +1,210 @@ +/* + * Copyright (c) 2026 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 debug implements the cross-platform "llgo debug" command. +package debug + +import ( + "errors" + "fmt" + "io" + "os" + "path/filepath" + "runtime" + "strings" + + "github.com/goplus/llgo/cmd/internal/base" + "github.com/goplus/llgo/cmd/internal/flags" + "github.com/goplus/llgo/internal/build" + "github.com/goplus/llgo/internal/mockable" + "github.com/goplus/llgo/internal/optlevel" + "github.com/goplus/llgo/internal/targets" +) + +// Cmd is the llgo debug command. +var Cmd = &base.Command{ + UsageLine: "llgo debug [-backend auto|lldb|gdb|wasmtime|browser] [-target platform] [build flags] [package] [-- debugger arguments...]", + Short: "Build and debug an LLGo program", +} + +var ( + goBuildFlags *base.PassArgs + backendFlag string + lldbPath string + gdbPath string + remoteAddress string + serverCommand string +) + +func init() { + Cmd.Run = runCmd + goBuildFlags = flags.CaptureGoBuildFlags(Cmd) + flags.AddCommonFlags(&Cmd.Flag) + flags.AddCompilerVerboseFlag(&Cmd.Flag) + flags.AddBuildFlags(&Cmd.Flag) + flags.AddEmbeddedFlags(&Cmd.Flag) + flags.AddOutputFlags(&Cmd.Flag) + Cmd.Flag.StringVar(&backendFlag, "backend", string(backendAuto), "debug backend: auto, lldb, gdb, wasmtime, or browser") + Cmd.Flag.StringVar(&lldbPath, "lldb", "", "path to LLDB (default $LLGO_LLDB or auto-detect)") + Cmd.Flag.StringVar(&gdbPath, "gdb", "", "path to GDB (default $LLGO_GDB, target candidates, or auto-detect)") + Cmd.Flag.StringVar(&remoteAddress, "remote", "", "connect to an existing debug server at host:port") + Cmd.Flag.StringVar(&serverCommand, "server", "", "debug-server command template; {} is the artifact and {debug-port} is the allocated port") +} + +func runCmd(cmd *base.Command, args []string) { + commandArgs, debuggerArgs := splitDebuggerArgs(args) + if err := cmd.Flag.Parse(commandArgs); err != nil { + mockable.Exit(2) + return + } + if err := run(cmd.Flag.Args(), debuggerArgs, options{ + backend: backend(backendFlag), + lldb: lldbPath, + gdb: gdbPath, + remote: remoteAddress, + server: serverCommand, + }, os.Stdin, os.Stdout, os.Stderr); err != nil { + fmt.Fprintln(os.Stderr, err) + mockable.Exit(1) + } +} + +func splitDebuggerArgs(args []string) (command, debugger []string) { + for i, arg := range args { + if arg == "--" { + return args[:i], args[i+1:] + } + } + return args, nil +} + +func run(packageArgs, debuggerArgs []string, opts options, stdin io.Reader, stdout, stderr io.Writer) error { + if len(packageArgs) > 1 { + return errors.New("llgo debug: exactly one package may be debugged") + } + if len(packageArgs) == 0 { + packageArgs = []string{"."} + } + if err := opts.validate(); err != nil { + return err + } + + conf := build.NewDefaultConf(build.ModeBuild) + if err := flags.UpdateConfig(conf); err != nil { + return fmt.Errorf("llgo debug: %w", err) + } + if err := flags.ApplyGoBuildFlags(conf, goBuildFlags.Args); err != nil { + return fmt.Errorf("llgo debug: %w", err) + } + conf.BuildMode = build.BuildModeExe + conf.OmitDWARFByDefault = false + if conf.LinkOptions.EffectiveOmitDWARF() || + (conf.DebugArtifactModeSet && conf.DebugArtifactMode == build.DebugArtifactNone) { + return errors.New("llgo debug: debug information is required; remove -ldflags=-w or -debug-artifact=none") + } + if conf.OptLevel == optlevel.Unset { + conf.OptLevel = optlevel.O0 + } + + target, err := resolveTarget(conf.Target) + if err != nil { + return err + } + selected, err := selectBackend(opts.backend, classifyTarget(conf, target)) + if err != nil { + return err + } + if selected == backendWasmtime { + return errors.New("llgo debug: the WASI/Wasmtime backend is not available yet") + } + if selected == backendBrowser { + return errors.New("llgo debug: the browser DevTools backend is not available yet") + } + if target == nil && opts.remote == "" && (conf.Goos != runtime.GOOS || conf.Goarch != runtime.GOARCH) { + return fmt.Errorf("llgo debug: cannot launch a %s/%s program on %s/%s without -remote", conf.Goos, conf.Goarch, runtime.GOOS, runtime.GOARCH) + } + + cleanup, artifact, err := prepareArtifact(conf) + if err != nil { + return err + } + defer cleanup() + if _, err = build.Do(packageArgs, conf); err != nil { + return err + } + if _, err = os.Stat(artifact); err != nil { + return fmt.Errorf("llgo debug: built artifact %q is unavailable: %w", artifact, err) + } + + return runSession(session{ + backend: selected, + artifact: artifact, + debuggerArgs: debuggerArgs, + target: target, + options: opts, + }, stdin, stdout, stderr) +} + +func resolveTarget(name string) (*targets.Config, error) { + if name == "" { + return nil, nil + } + target, err := targets.NewDefaultResolver().Resolve(name) + if err != nil { + return nil, fmt.Errorf("llgo debug: %w", err) + } + return target, nil +} + +func prepareArtifact(conf *build.Config) (cleanup func(), artifact string, err error) { + cleanup = func() {} + ext := debugArtifactExtension(conf) + if conf.OutFile == "" { + dir, err := os.MkdirTemp("", "llgo-debug-") + if err != nil { + return cleanup, "", fmt.Errorf("llgo debug: create artifact directory: %w", err) + } + cleanup = func() { os.RemoveAll(dir) } + conf.OutFile = filepath.Join(dir, "program"+ext) + } else if ext != "" && !strings.HasSuffix(conf.OutFile, ext) { + conf.OutFile += ext + } + conf.AppExt = ext + artifact, err = filepath.Abs(conf.OutFile) + if err != nil { + cleanup() + return func() {}, "", fmt.Errorf("llgo debug: resolve artifact path: %w", err) + } + conf.OutFile = artifact + return cleanup, artifact, nil +} + +func debugArtifactExtension(conf *build.Config) string { + if conf.Target != "" { + if strings.HasPrefix(conf.Target, "wasi") || strings.HasPrefix(conf.Target, "wasm") { + return ".wasm" + } + return ".elf" + } + switch conf.Goos { + case "windows": + return ".exe" + case "js", "wasi", "wasip1": + return ".wasm" + default: + return "" + } +} diff --git a/cmd/internal/debug/debug_test.go b/cmd/internal/debug/debug_test.go new file mode 100644 index 0000000000..146edd2f49 --- /dev/null +++ b/cmd/internal/debug/debug_test.go @@ -0,0 +1,339 @@ +//go:build !llgo + +/* + * Copyright (c) 2026 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 debug + +import ( + "bytes" + "fmt" + "net" + "os" + "path/filepath" + "runtime" + "strconv" + "strings" + "testing" + + "github.com/goplus/llgo/cmd/internal/flags" + "github.com/goplus/llgo/internal/build" + "github.com/goplus/llgo/internal/optlevel" + "github.com/goplus/llgo/internal/targets" +) + +func TestBackendRouting(t *testing.T) { + tests := []struct { + name string + conf build.Config + target *targets.Config + want backend + }{ + {name: "native", conf: build.Config{Goos: runtime.GOOS, Goarch: runtime.GOARCH}, want: backendLLDB}, + {name: "embedded", conf: build.Config{Target: "board"}, target: &targets.Config{LLVMTarget: "thumbv7m-none-eabi"}, want: backendGDB}, + {name: "WASI", conf: build.Config{Target: "wasip1"}, target: &targets.Config{GOOS: "wasip1", GOARCH: "wasm", LLVMTarget: "wasm32-unknown-wasi"}, want: backendWasmtime}, + {name: "browser", conf: build.Config{Target: "wasm"}, target: &targets.Config{GOOS: "js", GOARCH: "wasm", LLVMTarget: "wasm32-unknown-wasi"}, want: backendBrowser}, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + kind := classifyTarget(&test.conf, test.target) + got, err := selectBackend(backendAuto, kind) + if err != nil || got != test.want { + t.Fatalf("selectBackend(auto) = (%q, %v), want (%q, nil)", got, err, test.want) + } + }) + } + if _, err := selectBackend(backendGDB, targetWASI); err == nil { + t.Fatal("GDB unexpectedly accepted a WASI target") + } + if err := (options{backend: "unknown"}).validate(); err == nil { + t.Fatal("unknown backend was accepted") + } +} + +func TestSessionPlanning(t *testing.T) { + remote, err := makeServerPlan(nil, "program", options{remote: ":1234"}) + if err != nil || remote.address != "127.0.0.1:1234" || len(remote.command) != 0 { + t.Fatalf("remote plan = (%+v, %v)", remote, err) + } + + openocd, err := makeServerPlan(&targets.Config{ + Name: "board", + OpenOCDInterface: "cmsis-dap", + OpenOCDTransport: "swd", + OpenOCDTarget: "stm32f4x", + }, "program.elf", options{}) + if err != nil { + t.Fatal(err) + } + joined := strings.Join(openocd.command, " ") + for _, want := range []string{"openocd", "gdb_port", "interface/cmsis-dap.cfg", "transport select swd", "target/stm32f4x.cfg"} { + if !strings.Contains(joined, want) { + t.Fatalf("OpenOCD command %q does not contain %q", joined, want) + } + } + if !openocd.load { + t.Fatal("OpenOCD plan does not request image loading") + } + + gdbArgs, err := debuggerArguments(backendGDB, "program.elf", []string{"--batch"}, openocd) + if err != nil { + t.Fatal(err) + } + joined = strings.Join(gdbArgs, " ") + for _, want := range []string{"target extended-remote", "monitor reset halt", " load ", "--batch"} { + if !strings.Contains(" "+joined+" ", want) { + t.Fatalf("GDB arguments %q do not contain %q", joined, want) + } + } + if _, err := debuggerArguments(backendLLDB, "program.elf", nil, openocd); err == nil { + t.Fatal("LLDB unexpectedly accepted automated OpenOCD loading") + } + command, err := parseServerCommand("server -kernel {} -port {debug-port}", filepath.Join("dir with space", "program.elf"), 4321) + if err != nil || len(command) != 5 || command[2] != filepath.Join("dir with space", "program.elf") || command[4] != "4321" { + t.Fatalf("parseServerCommand() = (%v, %v)", command, err) + } + + lldbArgs, err := debuggerArguments(backendLLDB, `dir/program.elf`, []string{"--batch"}, remote) + if err != nil { + t.Fatal(err) + } + joined = strings.Join(lldbArgs, " ") + for _, want := range []string{"gdb-remote 127.0.0.1:1234", "target modules load", "--slide 0", "--batch"} { + if !strings.Contains(joined, want) { + t.Fatalf("LLDB arguments %q do not contain %q", joined, want) + } + } +} + +func TestGDBSessionStartsConfiguredServer(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("test helper uses a POSIX shell") + } + t.Setenv("LLGO_DEBUG_SERVER_HELPER", "1") + capture := filepath.Join(t.TempDir(), "gdb-arguments") + t.Setenv("LLGO_DEBUG_GDB_CAPTURE", capture) + gdbPath := writeFakeGDB(t) + template := fmt.Sprintf("%s -test.run=^TestDebugServerHelper$ -- {debug-port}", strconv.Quote(os.Args[0])) + + var stdout, stderr bytes.Buffer + err := runSession(session{ + backend: backendGDB, + artifact: filepath.Join(t.TempDir(), "program.elf"), + target: &targets.Config{ + Name: "test-target", + DebugServer: template, + GDB: []string{gdbPath}, + }, + options: options{backend: backendGDB}, + }, strings.NewReader(""), &stdout, &stderr) + if err != nil { + t.Fatalf("runSession() error: %v; stderr=%s", err, stderr.String()) + } + data, err := os.ReadFile(capture) + if err != nil { + t.Fatal(err) + } + args := string(data) + if !strings.Contains(args, "target remote 127.0.0.1:") || !strings.Contains(args, "program.elf") { + t.Fatalf("GDB arguments do not contain the artifact and remote session: %q", args) + } +} + +func TestDebugServerFailureIncludesOutput(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("test helper uses a POSIX shell") + } + serverPath := filepath.Join(t.TempDir(), "failing-server") + if err := os.WriteFile(serverPath, []byte("#!/bin/sh\necho 'server startup failed' >&2\nexit 7\n"), 0700); err != nil { + t.Fatal(err) + } + port, err := freeTCPPort() + if err != nil { + t.Fatal(err) + } + _, err = startServer(serverPlan{ + command: []string{serverPath}, + address: net.JoinHostPort("127.0.0.1", strconv.Itoa(port)), + }) + if err == nil || !strings.Contains(err.Error(), "server startup failed") { + t.Fatalf("startServer() error = %v", err) + } +} + +func TestDebugServerHelper(t *testing.T) { + if os.Getenv("LLGO_DEBUG_SERVER_HELPER") != "1" { + return + } + separator := -1 + for i, arg := range os.Args { + if arg == "--" { + separator = i + break + } + } + if separator < 0 || separator+1 >= len(os.Args) { + t.Fatal("missing helper port") + } + listener, err := net.Listen("tcp", "127.0.0.1:"+os.Args[separator+1]) + if err != nil { + t.Fatal(err) + } + defer listener.Close() + for { + connection, err := listener.Accept() + if err != nil { + return + } + connection.Close() + } +} + +func TestArtifactAndArgumentHandling(t *testing.T) { + command, debugger := splitDebuggerArgs([]string{"-target=board", ".", "--", "--batch", "-ex", "run"}) + if strings.Join(command, " ") != "-target=board ." || strings.Join(debugger, " ") != "--batch -ex run" { + t.Fatalf("splitDebuggerArgs() = (%v, %v)", command, debugger) + } + + conf := &build.Config{Target: "cortex-m-qemu", OutFile: filepath.Join(t.TempDir(), "firmware")} + cleanup, artifact, err := prepareArtifact(conf) + if err != nil { + t.Fatal(err) + } + defer cleanup() + if !strings.HasSuffix(artifact, "firmware.elf") || conf.OutFile != artifact || conf.AppExt != ".elf" { + t.Fatalf("prepareArtifact() = %q, config=(%q, %q)", artifact, conf.OutFile, conf.AppExt) + } + for _, test := range []struct { + conf build.Config + want string + }{ + {conf: build.Config{Goos: "windows"}, want: ".exe"}, + {conf: build.Config{Goos: "wasip1", Goarch: "wasm"}, want: ".wasm"}, + {conf: build.Config{Goos: "linux", Goarch: "amd64"}, want: ""}, + } { + if got := debugArtifactExtension(&test.conf); got != test.want { + t.Errorf("debugArtifactExtension(%+v) = %q, want %q", test.conf, got, test.want) + } + } + if target, err := resolveTarget("cortex-m-qemu"); err != nil || target.DebugServer == "" { + t.Fatalf("resolveTarget(cortex-m-qemu) = (%+v, %v)", target, err) + } +} + +func TestRunBuildsAndLaunchesNativeDebugger(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("test helper uses a POSIX shell") + } + repoRoot, err := filepath.Abs(filepath.Join("..", "..", "..")) + if err != nil { + t.Fatal(err) + } + t.Setenv("LLGO_ROOT", repoRoot) + moduleDir := t.TempDir() + if err := os.WriteFile(filepath.Join(moduleDir, "go.mod"), []byte("module debugcommandtest\n\ngo 1.20\n"), 0600); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(moduleDir, "main.go"), []byte("package main\nfunc main() { value := 42; println(value) }\n"), 0600); err != nil { + t.Fatal(err) + } + capture := filepath.Join(t.TempDir(), "lldb-arguments") + t.Setenv("LLGO_DEBUG_LLDB_CAPTURE", capture) + fakeLLDB := writeFakeLLDB(t) + + oldDir, err := os.Getwd() + if err != nil { + t.Fatal(err) + } + if err := os.Chdir(moduleDir); err != nil { + t.Fatal(err) + } + defer os.Chdir(oldDir) + + flags.Target = "" + flags.OutputFile = "" + flags.OptLevel = optlevel.Unset + flags.Tags = "" + flags.Verbose = false + flags.CompilerVerbose = false + goBuildFlags.Args = nil + var stdout, stderr bytes.Buffer + if err := run(nil, []string{"--batch"}, options{backend: backendAuto, lldb: fakeLLDB}, strings.NewReader(""), &stdout, &stderr); err != nil { + t.Fatalf("run() error: %v; stderr=%s", err, stderr.String()) + } + data, err := os.ReadFile(capture) + if err != nil { + t.Fatal(err) + } + lines := strings.Split(strings.TrimSpace(string(data)), "\n") + if len(lines) < 4 || lines[0] != "-o" || !strings.Contains(lines[1], "command script import") || lines[len(lines)-1] != "--batch" { + t.Fatalf("LLDB arguments = %q", string(data)) + } + artifact := lines[2] + if !strings.Contains(artifact, "llgo-debug-") { + t.Fatalf("LLDB artifact = %q, want temporary debug artifact", artifact) + } + if _, err := os.Stat(artifact); !os.IsNotExist(err) { + t.Fatalf("temporary artifact still exists after debugger exit: %v", err) + } + + if err := run([]string{".", "./other"}, nil, options{backend: backendAuto}, strings.NewReader(""), &stdout, &stderr); err == nil { + t.Fatal("multiple packages were accepted") + } + if err := run(nil, nil, options{backend: "invalid"}, strings.NewReader(""), &stdout, &stderr); err == nil { + t.Fatal("invalid backend was accepted") + } + goBuildFlags.Args = []string{"-ldflags=-s"} + if err := run(nil, nil, options{backend: backendAuto}, strings.NewReader(""), &stdout, &stderr); err == nil || !strings.Contains(err.Error(), "debug information is required") { + t.Fatalf("run(-ldflags=-s) error = %v", err) + } + goBuildFlags.Args = nil +} + +func writeFakeGDB(t *testing.T) string { + t.Helper() + path := filepath.Join(t.TempDir(), "gdb") + script := `#!/bin/sh +if [ "$1" = "--version" ]; then + echo 'GNU gdb (GDB) 15.1' + exit 0 +fi +if [ "$1" = "--batch" ] && [ "$2" = "--nx" ]; then + exit 0 +fi +printf '%s\n' "$@" > "$LLGO_DEBUG_GDB_CAPTURE" +` + if err := os.WriteFile(path, []byte(script), 0700); err != nil { + t.Fatal(err) + } + return path +} + +func writeFakeLLDB(t *testing.T) string { + t.Helper() + path := filepath.Join(t.TempDir(), "lldb") + script := `#!/bin/sh +if [ "$1" = "--version" ]; then + echo 'lldb version 19.1.0' + exit 0 +fi +printf '%s\n' "$@" > "$LLGO_DEBUG_LLDB_CAPTURE" +` + if err := os.WriteFile(path, []byte(script), 0700); err != nil { + t.Fatal(err) + } + return path +} diff --git a/cmd/internal/debug/session.go b/cmd/internal/debug/session.go new file mode 100644 index 0000000000..c22f310237 --- /dev/null +++ b/cmd/internal/debug/session.go @@ -0,0 +1,393 @@ +/* + * Copyright (c) 2026 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 debug + +import ( + "errors" + "fmt" + "io" + "net" + "os" + "os/exec" + "path/filepath" + "strconv" + "strings" + "time" + + "github.com/goplus/llgo/cmd/internal/gdb" + "github.com/goplus/llgo/cmd/internal/lldb" + "github.com/goplus/llgo/internal/build" + "github.com/goplus/llgo/internal/env" + "github.com/goplus/llgo/internal/shellparse" + "github.com/goplus/llgo/internal/targets" +) + +type backend string + +const ( + backendAuto backend = "auto" + backendLLDB backend = "lldb" + backendGDB backend = "gdb" + backendWasmtime backend = "wasmtime" + backendBrowser backend = "browser" +) + +type targetKind uint8 + +const ( + targetNative targetKind = iota + targetEmbedded + targetWASI + targetBrowser +) + +type options struct { + backend backend + lldb string + gdb string + remote string + server string +} + +func (o options) validate() error { + switch o.backend { + case backendAuto, backendLLDB, backendGDB, backendWasmtime, backendBrowser: + return nil + default: + return fmt.Errorf("llgo debug: unknown backend %q; use auto, lldb, gdb, wasmtime, or browser", o.backend) + } +} + +func classifyTarget(conf *build.Config, target *targets.Config) targetKind { + goos, goarch, llvmTarget := conf.Goos, conf.Goarch, "" + if target != nil { + goos, goarch, llvmTarget = target.GOOS, target.GOARCH, target.LLVMTarget + } + if goarch == "wasm" || strings.HasPrefix(llvmTarget, "wasm") { + if goos == "js" || strings.HasPrefix(conf.Target, "wasm") { + return targetBrowser + } + return targetWASI + } + if target != nil { + return targetEmbedded + } + return targetNative +} + +func selectBackend(requested backend, kind targetKind) (backend, error) { + if requested != backendAuto { + switch kind { + case targetWASI: + if requested != backendWasmtime { + return "", fmt.Errorf("llgo debug: backend %s cannot debug a WASI target; use wasmtime", requested) + } + case targetBrowser: + if requested != backendBrowser { + return "", fmt.Errorf("llgo debug: backend %s cannot debug a browser target; use browser", requested) + } + default: + if requested != backendLLDB && requested != backendGDB { + return "", fmt.Errorf("llgo debug: backend %s cannot debug this target", requested) + } + } + return requested, nil + } + switch kind { + case targetEmbedded: + return backendGDB, nil + case targetWASI: + return backendWasmtime, nil + case targetBrowser: + return backendBrowser, nil + default: + return backendLLDB, nil + } +} + +type session struct { + backend backend + artifact string + debuggerArgs []string + target *targets.Config + options options +} + +func runSession(s session, stdin io.Reader, stdout, stderr io.Writer) error { + plan, err := makeServerPlan(s.target, s.artifact, s.options) + if err != nil { + return err + } + args, err := debuggerArguments(s.backend, s.artifact, s.debuggerArgs, plan) + if err != nil { + return err + } + var server *debugServer + if plan != nil { + server, err = startServer(*plan) + if err != nil { + return err + } + defer server.stop() + } + + var debugErr error + switch s.backend { + case backendLLDB: + if err := lldb.Run(s.options.lldb, args, stdin, stdout, stderr); err != nil { + debugErr = fmt.Errorf("llgo debug: %w", err) + } + case backendGDB: + var candidates []string + if s.target != nil { + candidates = s.target.GDB + } + if err := gdb.Run(s.options.gdb, candidates, args, stdin, stdout, stderr); err != nil { + debugErr = err + } + default: + debugErr = fmt.Errorf("llgo debug: backend %s is not implemented", s.backend) + } + if debugErr != nil && server != nil { + if output := server.logSuffix(); output != "" { + return fmt.Errorf("%w\ndebug server output:%s", debugErr, output) + } + } + return debugErr +} + +type serverPlan struct { + command []string + address string + load bool +} + +func makeServerPlan(target *targets.Config, artifact string, opts options) (*serverPlan, error) { + if target == nil { + if opts.server != "" { + return nil, errors.New("llgo debug: -server requires -target") + } + if opts.remote == "" { + return nil, nil + } + return &serverPlan{address: normalizeRemoteAddress(opts.remote)}, nil + } + if opts.remote != "" { + if opts.server != "" { + return nil, errors.New("llgo debug: -remote and -server are mutually exclusive") + } + return &serverPlan{address: normalizeRemoteAddress(opts.remote)}, nil + } + + port, err := freeTCPPort() + if err != nil { + return nil, fmt.Errorf("llgo debug: allocate debug-server port: %w", err) + } + serverTemplate := opts.server + if serverTemplate == "" { + serverTemplate = target.DebugServer + } + if serverTemplate != "" { + command, err := parseServerCommand(serverTemplate, artifact, port) + if err != nil { + return nil, err + } + return &serverPlan{command: command, address: net.JoinHostPort("127.0.0.1", strconv.Itoa(port))}, nil + } + if target.OpenOCDInterface == "" && target.OpenOCDTarget == "" { + return nil, fmt.Errorf("llgo debug: target %s has no debug server; use -remote or -server", target.Name) + } + command := []string{"openocd", "-c", fmt.Sprintf("gdb_port %d", port)} + if target.OpenOCDInterface != "" { + command = append(command, "-f", "interface/"+target.OpenOCDInterface+".cfg") + } + if target.OpenOCDTransport != "" { + command = append(command, "-c", "transport select "+target.OpenOCDTransport) + } + if target.OpenOCDTarget != "" { + command = append(command, "-f", "target/"+target.OpenOCDTarget+".cfg") + } + return &serverPlan{ + command: command, + address: net.JoinHostPort("127.0.0.1", strconv.Itoa(port)), + load: true, + }, nil +} + +func normalizeRemoteAddress(address string) string { + address = strings.TrimPrefix(address, "tcp://") + if strings.HasPrefix(address, ":") { + return "127.0.0.1" + address + } + return address +} + +func parseServerCommand(template, artifact string, port int) ([]string, error) { + replacer := strings.NewReplacer( + "{debug-port}", strconv.Itoa(port), + "{root}", quoteServerArgument(env.LLGoROOT()), + "{tmpDir}", quoteServerArgument(os.TempDir()), + "{elf}", quoteServerArgument(artifact), + "{}", quoteServerArgument(artifact), + ) + command, err := shellparse.Parse(replacer.Replace(template)) + if err != nil { + return nil, fmt.Errorf("llgo debug: parse debug-server command: %w", err) + } + if len(command) == 0 { + return nil, errors.New("llgo debug: debug-server command is empty") + } + return command, nil +} + +func quoteServerArgument(value string) string { + return `"` + strings.NewReplacer(`\`, `\\`, `"`, `\"`).Replace(value) + `"` +} + +func freeTCPPort() (int, error) { + listener, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + return 0, err + } + defer listener.Close() + return listener.Addr().(*net.TCPAddr).Port, nil +} + +type debugServer struct { + cmd *exec.Cmd + done <-chan error + finished bool + log *os.File + logPath string +} + +func startServer(plan serverPlan) (*debugServer, error) { + if len(plan.command) == 0 { + return nil, nil + } + log, err := os.CreateTemp("", "llgo-debug-server-*.log") + if err != nil { + return nil, fmt.Errorf("llgo debug: create debug-server log: %w", err) + } + command := exec.Command(plan.command[0], plan.command[1:]...) + command.Stdout = log + command.Stderr = log + if err := command.Start(); err != nil { + log.Close() + os.Remove(log.Name()) + return nil, fmt.Errorf("llgo debug: start debug server %q: %w", plan.command[0], err) + } + done := make(chan error, 1) + go func() { done <- command.Wait() }() + server := &debugServer{cmd: command, done: done, log: log, logPath: log.Name()} + if err := server.waitReady(plan.address, 10*time.Second); err != nil { + server.stop() + return nil, err + } + return server, nil +} + +func (s *debugServer) waitReady(address string, timeout time.Duration) error { + deadline := time.Now().Add(timeout) + for time.Now().Before(deadline) { + select { + case err := <-s.done: + s.finished = true + return fmt.Errorf("llgo debug: debug server exited before listening at %s: %v%s", address, err, s.logSuffix()) + default: + } + connection, err := net.DialTimeout("tcp", address, 100*time.Millisecond) + if err == nil { + connection.Close() + time.Sleep(50 * time.Millisecond) + return nil + } + time.Sleep(25 * time.Millisecond) + } + return fmt.Errorf("llgo debug: timed out waiting for debug server at %s%s", address, s.logSuffix()) +} + +func (s *debugServer) stop() { + if s == nil { + return + } + if s.cmd != nil && s.cmd.Process != nil { + _ = s.cmd.Process.Kill() + } + if !s.finished { + select { + case <-s.done: + s.finished = true + case <-time.After(2 * time.Second): + } + } + if s.log != nil { + _ = s.log.Close() + } + if s.logPath != "" { + _ = os.Remove(s.logPath) + } +} + +func (s *debugServer) logSuffix() string { + if s.log != nil { + _ = s.log.Sync() + } + data, err := os.ReadFile(s.logPath) + if err != nil || len(strings.TrimSpace(string(data))) == 0 { + return "" + } + return "\n" + strings.TrimSpace(string(data)) +} + +func debuggerArguments(selected backend, artifact string, extra []string, server *serverPlan) ([]string, error) { + if server == nil { + return append([]string{artifact}, extra...), nil + } + if server.address == "" { + return nil, errors.New("llgo debug: remote debug-server address is empty") + } + switch selected { + case backendGDB: + args := []string{"--quiet", artifact} + remoteCommand := "target remote " + server.address + if server.load { + remoteCommand = "target extended-remote " + server.address + } + args = append(args, "-ex", remoteCommand) + if server.load { + args = append(args, "-ex", "monitor reset halt", "-ex", "load", "-ex", "monitor reset halt") + } + return append(args, extra...), nil + case backendLLDB: + if server.load { + return nil, errors.New("llgo debug: LLDB does not yet automate OpenOCD image loading; use -backend=gdb") + } + args := []string{ + artifact, + "-o", "gdb-remote " + server.address, + "-o", "target modules load --file " + quoteLLDBArgument(artifact) + " --slide 0", + } + return append(args, extra...), nil + default: + return nil, fmt.Errorf("llgo debug: backend %s does not use GDB Remote", selected) + } +} + +func quoteLLDBArgument(value string) string { + return `"` + strings.NewReplacer(`\`, `\\`, `"`, `\"`).Replace(filepath.ToSlash(value)) + `"` +} diff --git a/cmd/internal/lldb/lldb.go b/cmd/internal/lldb/lldb.go index dff50ef137..ec07ea76d5 100644 --- a/cmd/internal/lldb/lldb.go +++ b/cmd/internal/lldb/lldb.go @@ -114,6 +114,11 @@ func run(configuredPath string, args []string, stdin io.Reader, stdout, stderr i return nil } +// Run starts LLDB with the LLGo adapter for a higher-level debug session. +func Run(configuredPath string, args []string, stdin io.Reader, stdout, stderr io.Writer) error { + return run(configuredPath, args, stdin, stdout, stderr) +} + func findLLDB(configuredPath string) (string, error) { return findLLDBFrom(configuredPath, os.Getenv("LLGO_LLDB"), []string{ "/opt/homebrew/bin/lldb", diff --git a/cmd/llgo/debug_cmd.gox b/cmd/llgo/debug_cmd.gox new file mode 100644 index 0000000000..a731a0ad65 --- /dev/null +++ b/cmd/llgo/debug_cmd.gox @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2026 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. + */ + +import ( + self "github.com/goplus/llgo/cmd/internal/debug" +) + +use "debug [-backend auto|lldb|gdb|wasmtime|browser] [-target platform] [build flags] [package] [-- debugger arguments...]" + +short "Build and debug an LLGo program" + +flagOff + +run args => { + self.Cmd.Run self.Cmd, args +} diff --git a/cmd/llgo/debugtest/README.md b/cmd/llgo/debugtest/README.md new file mode 100644 index 0000000000..21fa58284e --- /dev/null +++ b/cmd/llgo/debugtest/README.md @@ -0,0 +1,33 @@ +# LLGo debug sessions + +`llgo debug` is the target-aware entry point for building and debugging an +LLGo program: + +```sh +llgo debug [build flags] [package] [-- debugger arguments...] +``` + +It enables DWARF, uses `-O0` unless an optimization level was selected, builds +one executable package, and owns any temporary artifact and local debug-server +process. An explicit `-o` keeps the artifact. `-ldflags=-w` and +`-debug-artifact=none` are rejected because the resulting program cannot be +source-debugged. + +The automatic backend depends on the selected target: + +| Target | Backend | Session transport | +| --- | --- | --- | +| Native Darwin/Linux | LLDB | Local process | +| Non-Wasm embedded | GDB | Target `debug-server`, OpenOCD, or `-remote` | +| WASI | Wasmtime | Added by the WASI debugger task | +| Browser Wasm | Browser DevTools | Added by the browser debugger task | + +Use `-backend=gdb` or `-backend=lldb` to override a native or GDB Remote +session, and `-gdb` or `-lldb` to select a debugger executable. For an already +running server, `-remote=host:port` skips server startup. A target's +`debug-server` command can use `{}` or `{elf}` for the host debug artifact and +`{debug-port}` for an automatically allocated loopback port. Targets with +OpenOCD interface/transport/target fields need no additional command. + +`llgo lldb` remains the explicit compatibility command for opening an existing +artifact without building it. diff --git a/cmd/llgo/debugtest/embedded/README.md b/cmd/llgo/debugtest/embedded/README.md index fb2c19f0d9..1987fc5a55 100644 --- a/cmd/llgo/debugtest/embedded/README.md +++ b/cmd/llgo/debugtest/embedded/README.md @@ -1,8 +1,8 @@ # Embedded debugger transport fixture -This fixture is the manual protocol baseline for LLGo embedded debugging. It -builds a host-side Cortex-M ELF with DWARF, derives unchanged flash bytes, and -uses the original ELF in two independent QEMU sessions: +This fixture validates the automated embedded path of `llgo debug`. It builds +a host-side Cortex-M ELF with DWARF, starts and stops the target-configured QEMU +GDB server, derives unchanged flash bytes, and runs two independent sessions: - `gdb-multiarch` through GDB Remote; - LLDB through `gdb-remote` with an explicit zero slide. @@ -14,14 +14,16 @@ LLVM 19 tools on `PATH`: bash cmd/llgo/debugtest/embedded/runtest.sh ``` -For a physical OpenOCD target, keep the same host ELF and connect the GDB -listed by the target configuration to the server's default port: +For a physical target with OpenOCD configuration, `llgo debug` starts OpenOCD, +loads the image, and connects the GDB listed by the target configuration: -```text -target extended-remote :3333 -monitor reset halt -load +```sh +llgo debug -target=rp2040 . ``` -The automated `llgo debug` process/server orchestration is intentionally built -on top of this artifact and transport contract rather than changing it. +To use an externally managed OpenOCD session instead, keep the same host ELF +and connect to its GDB port: + +```sh +llgo debug -target=rp2040 -remote=:3333 . +``` diff --git a/cmd/llgo/debugtest/embedded/runtest.sh b/cmd/llgo/debugtest/embedded/runtest.sh index 0ea04e1cbc..9643b41ada 100755 --- a/cmd/llgo/debugtest/embedded/runtest.sh +++ b/cmd/llgo/debugtest/embedded/runtest.sh @@ -5,13 +5,8 @@ set -euo pipefail script_dir=$(cd "$(dirname "$0")" && pwd) repo_root=$(cd "$script_dir/../../../.." && pwd) tmp_dir=$(mktemp -d "${TMPDIR:-/tmp}/llgo-embedded-debug.XXXXXX") -qemu_pid= cleanup() { - if [[ -n "$qemu_pid" ]]; then - kill "$qemu_pid" 2>/dev/null || true - wait "$qemu_pid" 2>/dev/null || true - fi rm -rf "$tmp_dir" } trap cleanup EXIT @@ -48,43 +43,13 @@ assert_contains() { fi } -free_port() { - python3 - <<'PY' -import socket - -with socket.socket() as sock: - sock.bind(("127.0.0.1", 0)) - print(sock.getsockname()[1]) -PY -} - -start_qemu() { - local port=$1 - "$qemu" -machine lm3s6965evb -nographic -kernel "$debug_elf" \ - -S -gdb "tcp::$port" >"$tmp_dir/qemu-$port.log" 2>&1 & - qemu_pid=$! - sleep 1 - if ! kill -0 "$qemu_pid" 2>/dev/null; then - cat "$tmp_dir/qemu-$port.log" >&2 - exit 1 - fi -} - -stop_qemu() { - if [[ -n "$qemu_pid" ]]; then - kill "$qemu_pid" 2>/dev/null || true - wait "$qemu_pid" 2>/dev/null || true - qemu_pid= - fi -} - llgo=$(require_tool llgo "${LLGO:-}" llgo) objcopy=$(require_tool llvm-objcopy "${LLVM_OBJCOPY:-}" llvm-objcopy llvm-objcopy-19) dwarfutil=$(require_tool llvm-dwarfutil "${LLVM_DWARFUTIL:-}" llvm-dwarfutil llvm-dwarfutil-19) dwarfdump=$(require_tool llvm-dwarfdump "${LLVM_DWARFDUMP:-}" llvm-dwarfdump llvm-dwarfdump-19) gdb=$(require_tool GDB "${LLGO_GDB:-}" gdb-multiarch arm-none-eabi-gdb gdb) lldb=$(require_tool LLDB "${LLGO_LLDB:-}" lldb-19 lldb) -qemu=$(require_tool qemu-system-arm "${LLGO_QEMU_SYSTEM_ARM:-}" qemu-system-arm) +require_tool qemu-system-arm qemu-system-arm >/dev/null debug_elf="$tmp_dir/embedded-debug.elf" stripped_elf="$tmp_dir/embedded-stripped.elf" @@ -98,32 +63,11 @@ if [[ -z "$break_line" ]]; then exit 1 fi -( - cd "$script_dir" - LLGO_ROOT="$repo_root" "$llgo" build -O0 -target=cortex-m-qemu \ - -debug-artifact=host -ldflags=-w=false -o "$debug_elf" . -) - -# Debug sections are host-only. Stripping them from the same final ELF must not -# alter the bytes that are loaded into target flash. -"$objcopy" --strip-debug "$debug_elf" "$stripped_elf" -"$objcopy" -O binary "$debug_elf" "$debug_bin" -"$objcopy" -O binary "$stripped_elf" "$stripped_bin" -cmp "$debug_bin" "$stripped_bin" - -# LLD section GC leaves tombstone DIEs for discarded functions. Verify a -# garbage-collected copy because LLVM 19 dwarfutil drops live global-variable -# DIEs as well; the original artifact below remains the debugger input. -"$dwarfutil" --garbage-collection --verify "$debug_elf" "$verified_elf" -"$dwarfdump" --verify "$verified_elf" - -gdb_port=$(free_port) -start_qemu "$gdb_port" -if ! gdb_output=$("$gdb" --nx --quiet --batch "$debug_elf" \ +if ! gdb_output=$(cd "$script_dir" && LLGO_ROOT="$repo_root" "$llgo" debug \ + -backend=gdb -gdb "$gdb" -target=cortex-m-qemu -o "$debug_elf" . -- \ + --nx --batch \ -ex "set pagination off" \ -ex "set confirm off" \ - -ex "source $repo_root/cmd/internal/gdb/llgo_plugin.py" \ - -ex "target remote 127.0.0.1:$gdb_port" \ -ex "break $source_file:$break_line" \ -ex "continue" \ -ex "llgo status" \ @@ -138,7 +82,6 @@ if ! gdb_output=$("$gdb" --nx --quiet --batch "$debug_elf" \ printf '%s\n' "$gdb_output" >&2 exit 1 fi -stop_qemu assert_contains "$gdb_output" "LLGo debugger schema v1 (runtime layout v1)" assert_contains "$gdb_output" '= "embedded"' assert_contains "$gdb_output" "LLGO_SEED=7" @@ -150,11 +93,9 @@ assert_contains "$gdb_output" "LLGO_SINK=33" assert_contains "$gdb_output" "Reset_Handler" assert_contains "$gdb_output" "C/c.go:$break_line" -lldb_port=$(free_port) -start_qemu "$lldb_port" -if ! lldb_output=$("$lldb" --batch "$debug_elf" \ - -o "gdb-remote 127.0.0.1:$lldb_port" \ - -o "target modules load --file $debug_elf --slide 0" \ +if ! lldb_output=$(cd "$script_dir" && LLGO_ROOT="$repo_root" "$llgo" debug \ + -backend=lldb -lldb "$lldb" -target=cortex-m-qemu -o "$debug_elf" . -- \ + --batch \ -o "breakpoint set --file c.go --line $break_line" \ -o "continue" \ -o "frame variable seed pair values text result" \ @@ -163,7 +104,6 @@ if ! lldb_output=$("$lldb" --batch "$debug_elf" \ printf '%s\n' "$lldb_output" >&2 exit 1 fi -stop_qemu if [[ "$lldb_output" == *"Traceback (most recent call last)"* ]]; then printf '%s\n' "$lldb_output" >&2 exit 1 @@ -176,4 +116,17 @@ assert_contains "$lldb_output" "DebugSink = 33" assert_contains "$lldb_output" "Reset_Handler" assert_contains "$lldb_output" "c.go:$break_line" -echo "embedded GDB Remote and LLDB gdb-remote checks passed" +# Debug sections are host-only. Stripping them from the same final ELF must not +# alter the bytes that are loaded into target flash. +"$objcopy" --strip-debug "$debug_elf" "$stripped_elf" +"$objcopy" -O binary "$debug_elf" "$debug_bin" +"$objcopy" -O binary "$stripped_elf" "$stripped_bin" +cmp "$debug_bin" "$stripped_bin" + +# LLD section GC leaves tombstone DIEs for discarded functions. Verify a +# garbage-collected copy because LLVM 19 dwarfutil drops live global-variable +# DIEs as well; the original artifact above remains the debugger input. +"$dwarfutil" --garbage-collection --verify "$debug_elf" "$verified_elf" +"$dwarfdump" --verify "$verified_elf" + +echo "llgo debug embedded GDB Remote and LLDB gdb-remote checks passed" diff --git a/cmd/llgo/lldbtest/README.md b/cmd/llgo/lldbtest/README.md index 1f3833cd2d..ae63ed5f49 100644 --- a/cmd/llgo/lldbtest/README.md +++ b/cmd/llgo/lldbtest/README.md @@ -23,7 +23,16 @@ requires an RFC and a long-term maintainer before restoring native Go language support, so LLGo keeps its language-specific adapter external; the decision is recorded in [issue #2154](https://github.com/xgo-dev/llgo/issues/2154). -### Debug with lldb +### Build and debug with LLDB + +For a source package, use the target-aware command. It enables DWARF and +defaults to `-O0` for reliable variable inspection: + +```shell +llgo debug ./cl/_testdata/debug +``` + +### Open an existing artifact with LLDB ```shell llgo lldb ./cl/_testdata/debug/out diff --git a/cmd/llgo/lldbtest/runtest.sh b/cmd/llgo/lldbtest/runtest.sh index 403a711b70..44133542b6 100755 --- a/cmd/llgo/lldbtest/runtest.sh +++ b/cmd/llgo/lldbtest/runtest.sh @@ -36,9 +36,6 @@ while [[ $# -gt 0 ]]; do esac done -# Build the project -build_project "$package_path" || exit 1 - # Set up private paths for test results and auxiliary fixtures. test_tmp_dir=$(mktemp -d "${TMPDIR:-/tmp}/llgo-lldbtest.XXXXXX") trap 'rm -rf "$test_tmp_dir"' EXIT @@ -73,8 +70,9 @@ for cmd in "${lldb_commands[@]}"; do done cd "$package_path" -# Run LLDB with the embedded LLGo plugin and the test script. -llgo lldb -lldb "$LLDB_PATH" -- "${lldb_args[@]}" +# Build a debug artifact and run LLDB with the embedded LLGo plugin and the +# test script through the cross-platform session entry point. +llgo debug -backend=lldb -lldb "$LLDB_PATH" -o debug.out . -- "${lldb_args[@]}" # Read the exit code from the result file if [ -f "$result_file" ]; then diff --git a/cmd/llgo/xgo_autogen.go b/cmd/llgo/xgo_autogen.go index 076f778ffb..0733b085e0 100644 --- a/cmd/llgo/xgo_autogen.go +++ b/cmd/llgo/xgo_autogen.go @@ -8,6 +8,7 @@ import ( "github.com/goplus/llgo/cmd/internal/build" "github.com/goplus/llgo/cmd/internal/clean" "github.com/goplus/llgo/cmd/internal/compile" + "github.com/goplus/llgo/cmd/internal/debug" "github.com/goplus/llgo/cmd/internal/install" "github.com/goplus/llgo/cmd/internal/lldb" "github.com/goplus/llgo/cmd/internal/monitor" @@ -32,6 +33,10 @@ type Cmd_cmptest struct { xcmd.Command *App } +type Cmd_debug struct { + xcmd.Command + *App +} type Cmd_get struct { xcmd.Command *App @@ -81,16 +86,17 @@ func (this *App) Main() { _xgo_obj0 := &Cmd_build{App: this} _xgo_obj1 := &Cmd_clean{App: this} _xgo_obj2 := &Cmd_cmptest{App: this} - _xgo_obj3 := &Cmd_get{App: this} - _xgo_obj4 := &Cmd_install{App: this} - _xgo_obj5 := &Cmd_lldb{App: this} - _xgo_obj6 := &Cmd_monitor{App: this} - _xgo_obj7 := &Cmd_run{App: this} - _xgo_obj8 := &Cmd_test{App: this} - _xgo_obj9 := &Cmd_tool{App: this} - _xgo_obj10 := &Cmd_tool_compile{App: this} - _xgo_obj11 := &Cmd_version{App: this} - xcmd.Gopt_App_Main(this, _xgo_obj0, _xgo_obj1, _xgo_obj2, _xgo_obj3, _xgo_obj4, _xgo_obj5, _xgo_obj6, _xgo_obj7, _xgo_obj8, _xgo_obj9, _xgo_obj10, _xgo_obj11) + _xgo_obj3 := &Cmd_debug{App: this} + _xgo_obj4 := &Cmd_get{App: this} + _xgo_obj5 := &Cmd_install{App: this} + _xgo_obj6 := &Cmd_lldb{App: this} + _xgo_obj7 := &Cmd_monitor{App: this} + _xgo_obj8 := &Cmd_run{App: this} + _xgo_obj9 := &Cmd_test{App: this} + _xgo_obj10 := &Cmd_tool{App: this} + _xgo_obj11 := &Cmd_tool_compile{App: this} + _xgo_obj12 := &Cmd_version{App: this} + xcmd.Gopt_App_Main(this, _xgo_obj0, _xgo_obj1, _xgo_obj2, _xgo_obj3, _xgo_obj4, _xgo_obj5, _xgo_obj6, _xgo_obj7, _xgo_obj8, _xgo_obj9, _xgo_obj10, _xgo_obj11, _xgo_obj12) } //line cmd/llgo/build_cmd.gox:20 @@ -150,6 +156,25 @@ func (this *Cmd_cmptest) Classfname() string { return "cmptest" } +//line cmd/llgo/debug_cmd.gox:20 +func (this *Cmd_debug) Main(_xgo_arg0 string) { + this.Command.Main(_xgo_arg0) +//line cmd/llgo/debug_cmd.gox:20:1 + this.Use("debug [-backend auto|lldb|gdb|wasmtime|browser] [-target platform] [build flags] [package] [-- debugger arguments...]") +//line cmd/llgo/debug_cmd.gox:22:1 + this.Short("Build and debug an LLGo program") +//line cmd/llgo/debug_cmd.gox:24:1 + this.FlagOff() +//line cmd/llgo/debug_cmd.gox:26:1 + this.Run__1(func(args []string) { +//line cmd/llgo/debug_cmd.gox:27:1 + debug.Cmd.Run(debug.Cmd, args) + }) +} +func (this *Cmd_debug) Classfname() string { + return "debug" +} + //line cmd/llgo/get_cmd.gox:16 func (this *Cmd_get) Main(_xgo_arg0 string) { this.Command.Main(_xgo_arg0) diff --git a/internal/targets/config.go b/internal/targets/config.go index 1d56e7d6d4..47c6ffa094 100644 --- a/internal/targets/config.go +++ b/internal/targets/config.go @@ -51,8 +51,9 @@ type Config struct { RP2040BootPatch bool `json:"rp2040-boot-patch"` // Debug and emulation configuration - Emulator string `json:"emulator"` - GDB []string `json:"gdb"` + Emulator string `json:"emulator"` + DebugServer string `json:"debug-server"` + GDB []string `json:"gdb"` // OpenOCD configuration OpenOCDInterface string `json:"openocd-interface"` diff --git a/internal/targets/loader.go b/internal/targets/loader.go index 5603ddcd70..36f756114a 100644 --- a/internal/targets/loader.go +++ b/internal/targets/loader.go @@ -182,6 +182,9 @@ func (l *Loader) mergeConfig(dst, src *Config) { if src.Emulator != "" { dst.Emulator = src.Emulator } + if src.DebugServer != "" { + dst.DebugServer = src.DebugServer + } if src.OpenOCDInterface != "" { dst.OpenOCDInterface = src.OpenOCDInterface } diff --git a/targets/cortex-m-qemu.json b/targets/cortex-m-qemu.json index 5a1758dbfb..5a31eb7445 100644 --- a/targets/cortex-m-qemu.json +++ b/targets/cortex-m-qemu.json @@ -6,5 +6,6 @@ "extra-files": [ "targets/cortex-m-qemu.s" ], - "emulator": "qemu-system-arm -machine lm3s6965evb -semihosting -nographic -kernel {}" + "emulator": "qemu-system-arm -machine lm3s6965evb -semihosting -nographic -kernel {}", + "debug-server": "qemu-system-arm -machine lm3s6965evb -semihosting -nographic -S -gdb tcp:127.0.0.1:{debug-port} -kernel {}" }