Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/scripts/test_ci_workflows.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
WORKFLOWS = Path(__file__).resolve().parents[1] / "workflows"
PREPARE = "./.github/workflows/ci-prepare.yml"
CODE_WORKFLOWS = {
"llgo.yml": 20, "go.yml": 6, "targets.yml": 2, "build-cache.yml": 4,
"llgo.yml": 20, "go.yml": 6, "targets.yml": 8, "build-cache.yml": 4,
"benchmark.yml": 9, "release-build.yml": 15, "doc.yml": 6, "fmt.yml": 1,
}

Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/llgo.yml
Original file line number Diff line number Diff line change
Expand Up @@ -799,7 +799,7 @@ jobs:
LLGO_BUILD_CACHE: "1"
run: |
python3 -m unittest discover -s dev -p test_wasm_debug_info_test.py
python3 dev/test_wasm_debug_info.py --profile j32 --profile j64 --profile w32
python3 dev/test_wasm_debug_info.py --profile j32 --profile j64 --profile w32 --artifact embedded --artifact external

- name: Build standard runtime for wasm
if: matrix.suite == 'runtime'
Expand Down
75 changes: 74 additions & 1 deletion .github/workflows/targets.yml
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,81 @@ jobs:
- name: Install
run: |
go install ./...
echo "LLGO_ROOT=$GITHUB_WORKSPACE" >> $GITHUB_ENV
echo "LLGO_ROOT=$GITHUB_WORKSPACE" >> "$GITHUB_ENV"

- name: Build targets (${{ matrix.test_dir }})
working-directory: _demo/embed/targetsbuild
run: bash build.sh "${{ matrix.test_dir }}"

embedded-debug:
if: needs.prepare.outputs.run_code_ci == 'true'
needs: prepare
name: embedded debug (${{ matrix.os }}, ${{ matrix.backend }})
timeout-minutes: 40
strategy:
fail-fast: false
matrix:
os: [ubuntu-24.04, macos-15, windows-2022]
backend: [gdb, lldb]
runs-on: ${{ matrix.os == 'ubuntu-24.04' && fromJSON(needs.prepare.outputs.linux_large_runner) || matrix.os }}
env:
GOMAXPROCS: '4'
GOFLAGS: '-p=2'
steps:
- uses: actions/checkout@v7
- uses: ./.github/actions/setup-deps
with:
llvm-version: '22'
- uses: ./.github/actions/setup-go
- name: Build compiler
shell: bash
run: go install ./cmd/llgo

# Install after building: this action's MSYS2 shell belongs to debugger
# tools, not the pinned compiler environment installed by setup-deps.
- uses: ./.github/actions/setup-gdb
- name: Install Linux emulator and LLDB
if: runner.os == 'Linux'
run: sudo apt-get install -y qemu-system-arm lldb-22
- name: Install macOS emulator
if: runner.os == 'macOS'
run: brew install qemu
- name: Install Windows emulator in the isolated debugger environment
if: runner.os == 'Windows'
shell: msys2 {0}
run: |
set -euo pipefail
pacman --noconfirm -S --needed mingw-w64-clang-x86_64-qemu
echo "QEMU_SYSTEM_ARM=$(cygpath -m /clang64/bin/qemu-system-arm.exe)" >> "$GITHUB_ENV"
- name: Install matching Windows LLDB Python runtime
if: runner.os == 'Windows' && matrix.backend == 'lldb'
id: lldb-python
uses: actions/setup-python@v7
with:
# The LLVM 22.1.8 Windows LLDB installer links python311.dll.
python-version: '3.11.9'
architecture: x64
update-environment: false
- name: Configure matching Windows LLDB Python runtime
if: runner.os == 'Windows' && matrix.backend == 'lldb'
shell: pwsh
env:
LLDB_PYTHON: ${{ steps.lldb-python.outputs.python-path }}
run: |
$prefix = (& $env:LLDB_PYTHON -c "import sys; print(sys.base_prefix)").Trim()
if (-not (Test-Path (Join-Path $prefix 'python311.dll'))) {
throw "The LLVM 22 LLDB Python runtime is incomplete: $prefix"
}
"LLGO_WINDOWS_LLDB_PYTHON_HOME=$prefix" >> $env:GITHUB_ENV
- name: Test preloaded and explicit-download Cortex-M sessions
shell: bash
run: python3 test/debug/embedded/runtest.py --backend "${{ matrix.backend }}"
- name: Test physical-probe opt-in guard
shell: bash
run: |
set +e
output="$(bash test/debug/hardware/runtest.sh 2>&1)"
result=$?
set -e
test "${result}" -eq 2
grep -F 'LLGO_HARDWARE_CONFIRM=flash' <<<"${output}"
224 changes: 224 additions & 0 deletions cmd/internal/debug/debug.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,224 @@
/*
* 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/xgo-dev/llgo/cmd/internal/base"
"github.com/xgo-dev/llgo/cmd/internal/flags"
"github.com/xgo-dev/llgo/internal/build"
"github.com/xgo-dev/llgo/internal/mockable"
"github.com/xgo-dev/llgo/internal/optlevel"
"github.com/xgo-dev/llgo/internal/targets"
)

// Cmd is the llgo debug command.
var Cmd = &base.Command{
UsageLine: "llgo debug [-backend auto|lldb|gdb] [-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
loadImage bool
)

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, or gdb; wasmtime and browser are reserved and unavailable")
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 template: {} is the artifact, {debug-stdio} selects RSP over stdin/stdout, {debug-port} selects legacy TCP")
Cmd.Flag.BoolVar(&loadImage, "load", false, "reset, load the image, and halt an existing remote or custom debug server")
}

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,
load: loadImage,
}, 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")
}
// Debug sessions require DWARF even when the selected target omits it by
// default. Keep explicit stripping requests above as user-facing errors.
conf.LinkOptions.DWARF = build.DWARFPreserve
if conf.OptLevel == optlevel.Unset {
conf.OptLevel = optlevel.O0
}

target, err := resolveTarget(conf.Target)
if err != nil {
return err
}
if target != nil {
if target.GOOS != "" {
conf.Goos = target.GOOS
}
if target.GOARCH != "" {
conf.Goarch = target.GOARCH
}
}
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 conf.Goarch == "wasm" || strings.HasPrefix(conf.Target, "wasi") || strings.HasPrefix(conf.Target, "wasm") || strings.HasPrefix(conf.Target, "emscripten") {
return ".wasm"
}
return ".elf"
}
switch conf.Goos {
case "windows":
return ".exe"
case "js", "wasi", "wasip1":
return ".wasm"
default:
return ""
}
}
Loading
Loading