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
77 changes: 77 additions & 0 deletions .github/workflows/wasi-debug.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
name: WASI Debug

on:
push:
branches: [main]
paths:
- ".github/workflows/wasi-debug.yml"
- "cmd/internal/debug/**"
- "cmd/internal/lldb/**"
- "cmd/internal/wasmtime/**"
- "cmd/llgo/debugtest/wasi/**"
- "internal/debugabi/**"
- "internal/wasmdebug/**"
pull_request:
branches: ["**"]
paths:
- ".github/workflows/wasi-debug.yml"
- "cmd/internal/debug/**"
- "cmd/internal/lldb/**"
- "cmd/internal/wasmtime/**"
- "cmd/llgo/debugtest/wasi/**"
- "internal/debugabi/**"
- "internal/wasmdebug/**"
workflow_dispatch:

concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true

jobs:
guest-debug:
name: Wasmtime 47 / LLDB 22 guest debug
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@v7

- name: Install LLGo dependencies
uses: ./.github/actions/setup-deps
with:
llvm-version: 19

- name: Set up Go
uses: ./.github/actions/setup-go

- name: Cache WASI crosscompile toolchain
uses: actions/cache@v5
with:
path: ~/.cache/llgo/crosscompile
key: wasi-debug-${{ runner.os }}-${{ runner.arch }}-v25

- name: Install guest-debug tools
shell: bash
run: |
set -euo pipefail
mkdir -p .tools
curl -fL --retry 3 \
https://github.com/bytecodealliance/wasmtime/releases/download/v47.0.3/wasmtime-v47.0.3-x86_64-linux.tar.xz \
| tar -xJ -C .tools
curl -fL --retry 3 \
https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-33/wasi-sdk-33.0-x86_64-linux.tar.gz \
| tar -xz -C .tools
echo "LLGO_WASMTIME=${GITHUB_WORKSPACE}/.tools/wasmtime-v47.0.3-x86_64-linux/wasmtime" >> "${GITHUB_ENV}"
echo "LLGO_LLDB=${GITHUB_WORKSPACE}/.tools/wasi-sdk-33.0-x86_64-linux/bin/lldb" >> "${GITHUB_ENV}"
echo "LLGO_ROOT=${GITHUB_WORKSPACE}" >> "${GITHUB_ENV}"

- name: Test session planning and debugger contracts
run: |
go test -timeout 10m \
./internal/debugabi \
./internal/wasmdebug \
./cmd/internal/wasmtime \
./cmd/internal/lldb \
./cmd/internal/debug

- name: Test WASI source-debug session
run: bash cmd/llgo/debugtest/wasi/runtest.sh
32 changes: 24 additions & 8 deletions cmd/internal/debug/debug.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ var (
backendFlag string
lldbPath string
gdbPath string
wasmtimePath string
remoteAddress string
serverCommand string
)
Expand All @@ -60,6 +61,7 @@ func init() {
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(&wasmtimePath, "wasmtime", "", "path to Wasmtime (default $LLGO_WASMTIME 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")
}
Expand All @@ -71,11 +73,12 @@ func runCmd(cmd *base.Command, args []string) {
return
}
if err := run(cmd.Flag.Args(), debuggerArgs, options{
backend: backend(backendFlag),
lldb: lldbPath,
gdb: gdbPath,
remote: remoteAddress,
server: serverCommand,
backend: backend(backendFlag),
lldb: lldbPath,
gdb: gdbPath,
wasmtime: wasmtimePath,
remote: remoteAddress,
server: serverCommand,
}, os.Stdin, os.Stdout, os.Stderr); err != nil {
fmt.Fprintln(os.Stderr, err)
mockable.Exit(1)
Expand Down Expand Up @@ -123,13 +126,11 @@ func run(packageArgs, debuggerArgs []string, opts options, stdin io.Reader, stdo
if err != nil {
return err
}
applyResolvedTarget(conf, target)
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")
}
Expand Down Expand Up @@ -158,6 +159,21 @@ func run(packageArgs, debuggerArgs []string, opts options, stdin io.Reader, stdo
}, stdin, stdout, stderr)
}

func applyResolvedTarget(conf *build.Config, target *targets.Config) {
if conf == nil || target == nil {
return
}
// The existing wasm/wasi target names intentionally use the GOOS/GOARCH
// crosscompile path instead of the generic target-libc builder. Resolve
// those values before build.Do so debug-artifact selection and the
// crosscompiler agree on the target from the start.
if target.GOARCH == "wasm" {
conf.Goos = target.GOOS
conf.Goarch = target.GOARCH
conf.Target = ""
}
}

func resolveTarget(name string) (*targets.Config, error) {
if name == "" {
return nil, nil
Expand Down
127 changes: 127 additions & 0 deletions cmd/internal/debug/debug_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,10 @@ import (

"github.com/goplus/llgo/cmd/internal/flags"
"github.com/goplus/llgo/internal/build"
"github.com/goplus/llgo/internal/debugabi"
"github.com/goplus/llgo/internal/optlevel"
"github.com/goplus/llgo/internal/targets"
"github.com/goplus/llgo/internal/wasmdebug"
)

func TestBackendRouting(t *testing.T) {
Expand Down Expand Up @@ -234,6 +236,131 @@ func TestArtifactAndArgumentHandling(t *testing.T) {
}
}

func TestResolvedWasmTargetConfig(t *testing.T) {
conf := &build.Config{Goos: runtime.GOOS, Goarch: runtime.GOARCH, Target: "wasip1"}
applyResolvedTarget(conf, &targets.Config{GOOS: "wasip1", GOARCH: "wasm"})
if conf.Goos != "wasip1" || conf.Goarch != "wasm" || conf.Target != "" {
t.Fatalf("resolved target config = %s/%s target=%q, want wasip1/wasm GOOS path", conf.Goos, conf.Goarch, conf.Target)
}

native := &build.Config{Goos: runtime.GOOS, Goarch: runtime.GOARCH, Target: "board"}
applyResolvedTarget(native, &targets.Config{GOOS: "none", GOARCH: "arm"})
if native.Goos != runtime.GOOS || native.Goarch != runtime.GOARCH {
t.Fatalf("non-Wasm target unexpectedly changed config to %s/%s", native.Goos, native.Goarch)
}
}

func TestWASIDebugArtifactAndEnvironment(t *testing.T) {
raw := wasiDebugFixture(t)
artifact := filepath.Join(t.TempDir(), "program.wasm")
if err := os.WriteFile(artifact, raw, 0600); err != nil {
t.Fatal(err)
}
memory, err := validateWASIDebugArtifact(artifact)
if err != nil {
t.Fatal(err)
}
if memory == nil || memory.Module != "env" || memory.Name != "memory" || memory.Minimum != 1024 || memory.Maximum != 1024 || !memory.HasMax || !memory.Shared {
t.Fatalf("validated memory = %+v", memory)
}

environment, cleanup, err := writeWASIEnvironment(memory)
if err != nil {
t.Fatal(err)
}
data, err := os.ReadFile(environment)
if err != nil {
t.Fatal(err)
}
for _, want := range []string{`(memory (export "memory") 1024 1024 shared)`, `export "longjmp"`, `export "pthread_exit"`} {
if !strings.Contains(string(data), want) {
t.Fatalf("environment WAT %q does not contain %q", string(data), want)
}
}
cleanup()
if _, err := os.Stat(environment); !os.IsNotExist(err) {
t.Fatalf("environment cleanup error = %v", err)
}
withoutMemory := []byte{0, 'a', 's', 'm', 1, 0, 0, 0}
debug := appendTestName(nil, ".debug_info")
debug = append(debug, 1)
withoutMemory = appendTestSection(withoutMemory, 0, debug)
withoutMemory, err = wasmdebug.SetDebuggerRecord(withoutMemory, debugabi.NewRecord(2, 4, debugabi.ByteOrderLittle))
if err != nil {
t.Fatal(err)
}
withoutMemoryPath := filepath.Join(t.TempDir(), "self-contained.wasm")
if err := os.WriteFile(withoutMemoryPath, withoutMemory, 0600); err != nil {
t.Fatal(err)
}
if memory, err := validateWASIDebugArtifact(withoutMemoryPath); err != nil || memory != nil {
t.Fatalf("self-contained WASI artifact = (%+v, %v), want (nil, nil)", memory, err)
}

plan, planCleanup, err := makeWASIServerPlan(artifact, options{remote: ":1234"})
if err != nil {
t.Fatal(err)
}
defer planCleanup()
if plan.address != "127.0.0.1:1234" || len(plan.command) != 0 {
t.Fatalf("remote WASI plan = %+v", plan)
}
args, err := debuggerArguments(backendWasmtime, artifact, []string{"--batch"}, plan)
if err != nil {
t.Fatal(err)
}
joined := strings.Join(args, " ")
if !strings.Contains(joined, "process connect --plugin wasm connect://127.0.0.1:1234") || !strings.Contains(joined, "--batch") {
t.Fatalf("Wasmtime LLDB arguments = %q", joined)
}
}

func wasiDebugFixture(t *testing.T) []byte {
t.Helper()
imports := appendTestULEB(nil, 1)
imports = appendTestName(imports, "env")
imports = appendTestName(imports, "memory")
imports = append(imports, 2)
imports = appendTestULEB(imports, 3)
imports = appendTestULEB(imports, 1024)
imports = appendTestULEB(imports, 1024)
raw := []byte{0, 'a', 's', 'm', 1, 0, 0, 0}
raw = appendTestSection(raw, 2, imports)
debug := appendTestName(nil, ".debug_info")
debug = append(debug, 1, 2, 3)
raw = appendTestSection(raw, 0, debug)
result, err := wasmdebug.SetDebuggerRecord(raw, debugabi.NewRecord(2, 4, debugabi.ByteOrderLittle))
if err != nil {
t.Fatal(err)
}
return result
}

func appendTestName(dst []byte, name string) []byte {
dst = appendTestULEB(dst, uint32(len(name)))
return append(dst, name...)
}

func appendTestSection(dst []byte, id byte, payload []byte) []byte {
dst = append(dst, id)
dst = appendTestULEB(dst, uint32(len(payload)))
return append(dst, payload...)
}

func appendTestULEB(dst []byte, value uint32) []byte {
for {
current := byte(value & 0x7f)
value >>= 7
if value != 0 {
current |= 0x80
}
dst = append(dst, current)
if value == 0 {
return dst
}
}
}

func TestRunBuildsAndLaunchesNativeDebugger(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("test helper uses a POSIX shell")
Expand Down
Loading
Loading