diff --git a/.github/workflows/browser-debug.yml b/.github/workflows/browser-debug.yml index ec6db7fe6f..f610d3c5ba 100644 --- a/.github/workflows/browser-debug.yml +++ b/.github/workflows/browser-debug.yml @@ -47,6 +47,29 @@ jobs: with: version: "4.0.21" + - name: Build Binaryen with final-DWARF repair + shell: bash + env: + BINARYEN_REVISION: 0c439cce601d5812209df1fc1188afe90100fca6 + run: | + set -euo pipefail + source_dir="${GITHUB_WORKSPACE}/.tools/binaryen" + build_dir="${GITHUB_WORKSPACE}/.tools/binaryen-build" + git init "${source_dir}" + git -C "${source_dir}" remote add origin \ + https://github.com/cpunion/binaryen.git + git -C "${source_dir}" fetch --depth=1 origin \ + "${BINARYEN_REVISION}" + git -C "${source_dir}" checkout --detach FETCH_HEAD + test "$(git -C "${source_dir}" rev-parse HEAD)" = \ + "${BINARYEN_REVISION}" + cmake -S "${source_dir}" -B "${build_dir}" -G Ninja \ + -DCMAKE_BUILD_TYPE=Release -DBUILD_TESTS=OFF + cmake --build "${build_dir}" \ + --target wasm-opt wasm-emscripten-finalize --parallel 2 + "${build_dir}/bin/wasm-opt" --version + echo "EM_BINARYEN_ROOT=${build_dir}" >> "${GITHUB_ENV}" + - name: Set up Go uses: ./.github/actions/setup-go @@ -100,6 +123,19 @@ jobs: test -s "${RUNNER_TEMP}/browser-external.debug.wasm" ls -lh "${RUNNER_TEMP}"/browser-*.wasm + - name: Verify final browser DWARF + shell: bash + run: | + set -euo pipefail + for artifact in \ + "${RUNNER_TEMP}/browser-embedded.wasm" \ + "${RUNNER_TEMP}/browser-external.debug.wasm" + do + log="${artifact}.verify.log" + llvm-dwarfdump --verify "${artifact}" 2>&1 | tee "${log}" + grep -Fxq 'No errors.' "${log}" + done + - name: Test headless Chrome Language Extension shell: bash run: | diff --git a/.github/workflows/llgo.yml b/.github/workflows/llgo.yml index 4898c04aec..890fd5ab6b 100644 --- a/.github/workflows/llgo.yml +++ b/.github/workflows/llgo.yml @@ -132,6 +132,7 @@ jobs: run: | echo "Test lldb with llgo plugin on ${{matrix.os}} with LLVM ${{matrix.llvm}}" bash cmd/llgo/lldbtest/runtest.sh -v + bash cmd/llgo/debugtest/native/runtest.sh - name: DWARF standard tests if: ${{ startsWith(matrix.os, 'macos') && matrix.os != 'macos-15-intel' }} @@ -409,3 +410,13 @@ jobs: grep -q 'external_debug_info' "$RUNNER_TEMP/runtime-external.sections" ! grep -q '\.debug_info' "$RUNNER_TEMP/runtime-external.sections" grep -q '\.debug_info' "$RUNNER_TEMP/runtime-external-debug.sections" + + for artifact in \ + "$RUNNER_TEMP/runtime-debug.wasm" \ + "$RUNNER_TEMP/runtime-external.debug.wasm" + do + log="${artifact}.verify.log" + llvm-dwarfdump --verify --error-display=quiet \ + "${artifact}" 2>&1 | tee "${log}" + grep -Fxq 'No errors.' "${log}" + done diff --git a/cmd/llgo/debugtest/ACCEPTANCE.md b/cmd/llgo/debugtest/ACCEPTANCE.md index 02182506d5..e33491b498 100644 --- a/cmd/llgo/debugtest/ACCEPTANCE.md +++ b/cmd/llgo/debugtest/ACCEPTANCE.md @@ -24,13 +24,20 @@ compatibility target. Exact commands live in these focused fixtures: - WASI: `cmd/llgo/debugtest/wasi`; - browser: `cmd/internal/browser` and `internal/browserdebug`. +The browser lane uses Emscripten 4.0.21 and temporarily pins Binaryen commit +`0c439cce601d5812209df1fc1188afe90100fca6`, which contains the DWARF range +repair proposed in +[WebAssembly/binaryen#8964](https://github.com/WebAssembly/binaryen/pull/8964). +The pin can move back to an Emscripten-provided Binaryen release once that +repair is upstream and included in the supported SDK. + ## CI baseline | Gate | Workflow | What must be observed | | --- | --- | --- | -| Native Darwin/Linux | `llgo.yml` | source breakpoints, scopes, locals/globals, runtime values, goroutine ownership/stacks, and raw non-LLGo fallback | +| Native Darwin/Linux | `llgo.yml` | source breakpoints, scopes, locals/globals, runtime values, goroutine ownership/stacks, panic/fault source locations, optimized inline stepping, Go/C/Go frames, and raw non-LLGo fallback | | Emulated embedded | `targets.yml` | host ELF, GDB Remote and LLDB `gdb-remote`, source values/backtrace, identical flashed bytes with/without host DWARF, and verified retained DWARF | -| WASI | `wasi-debug.yml` | final Wasm DWARF, Wasmtime guest stub, source breakpoint, parameter/local, call stack, and clean exit | +| WASI | `wasi-debug.yml`, `llgo.yml` | strict final embedded/external Wasm DWARF verification, Wasmtime guest stub, source breakpoint, parameter/local, call stack, and clean exit | | Browser | `browser-debug.yml` | embedded and external DWARF, standard build identity, escaped sidecar URL, source remapping, all common runtime-layout categories, real LLGo Wasm registration, and no-extension fallback | Physical hardware is deliberately outside required PR CI. Its guarded script @@ -58,21 +65,23 @@ Platform-specific limits remain explicit: - A missing external Wasm sidecar and a mismatched `build_id` fail before the browser launches. A consumer without the LLGo extension keeps raw/name section symbolication but does not get Go runtime presentation. -- The current Asyncify-transformed browser runtime is accepted by Go's DWARF - reader and Chrome, but LLVM's final `llvm-dwarfdump --verify` still reports - overlapping/range-containment diagnostics. With Emscripten 4.0.21 and - Binaryen 125, relinking the exact same objects without Asyncify passes the - verifier across all compile units; Asyncify alone introduces invalid ranges - in both LLGo and Emscripten C-library DIEs. This matches Binaryen issue - [#6406](https://github.com/WebAssembly/binaryen/issues/6406). A source map - can recover source lines but cannot repair Wasm-local variable locations, so - it is not a substitute for this gate. This is a final-artifact blocker, not a - condition that the acceptance lane may suppress. - -Panic/trap source stops, optimized inline stepping, Go/host boundary frames, -and clean final DWARF after every LTO/post-link transform remain required -before the umbrella issue itself can be closed. Keep those failures visible in -focused fixtures rather than weakening the validators. +- Binaryen updates DWARF after Asyncify with a conservative range-topology + repair. Representable parent scopes are rebuilt from surviving children; + lost, reversed, or ambiguous scopes are reported as unavailable instead of + being assigned unrelated code. The browser lane runs + `llvm-dwarfdump --verify` against both the final embedded module and the + external DWARF sidecar after all post-link transforms. A verifier diagnostic + remains a hard failure; source maps are not accepted as a substitute for + variable, scope, or type DWARF. + +The native lane additionally stops explicit panic, integer division by zero, +invalid memory, and a real host trap at their exact source locations. Its +optimized fixture must preserve nested LLVM inline frames and step back to the +physical caller, while its callback fixture preserves ordered Go/C/Go frames. +Equivalent boundary coverage remains platform-specific. Clean final DWARF +after every LTO/post-link transform is still required before the umbrella +issue itself can be closed; keep transform failures visible in focused +fixtures rather than weakening the validators. ## Artifact-size accounting diff --git a/cmd/llgo/debugtest/native/README.md b/cmd/llgo/debugtest/native/README.md new file mode 100644 index 0000000000..f58ad681da --- /dev/null +++ b/cmd/llgo/debugtest/native/README.md @@ -0,0 +1,15 @@ +# Native fault and boundary debugging + +This fixture builds one final native LLGo artifact and drives it through stock +LLDB. It verifies that explicit panic, integer division by zero, and invalid +memory retain their exact Go source locations at the shared panic path; that a +real host trap stops on both its C source and Go caller; and that a callback +stack preserves ordered Go-to-C-to-Go boundary frames. A separate `-O2` +artifact must expose nested LLVM inline frames and step from the inline leaf +back to the non-inline caller's next source line. + +Run it after installing LLGo and LLDB 18 or newer: + +```sh +bash cmd/llgo/debugtest/native/runtest.sh +``` diff --git a/cmd/llgo/debugtest/native/acceptance.py b/cmd/llgo/debugtest/native/acceptance.py new file mode 100644 index 0000000000..4fc5d1b3aa --- /dev/null +++ b/cmd/llgo/debugtest/native/acceptance.py @@ -0,0 +1,287 @@ +"""Native LLGo source-debug acceptance for faults and Go/host frames.""" + +from pathlib import Path +from typing import Iterable, List + +import lldb + + +class AcceptanceError(RuntimeError): + pass + + +def marker_line(path: Path, marker: str) -> int: + matches = [ + number + for number, line in enumerate(path.read_text(encoding="utf-8").splitlines(), 1) + if marker in line + ] + if len(matches) != 1: + raise AcceptanceError( + f"expected one {marker!r} marker in {path}, found {len(matches)}" + ) + return matches[0] + + +def frame_name(frame: lldb.SBFrame) -> str: + return frame.GetFunctionName() or frame.GetDisplayFunctionName() or "" + + +def frame_location(frame: lldb.SBFrame) -> tuple[str, int]: + entry = frame.GetLineEntry() + if not entry.IsValid(): + return "", 0 + return entry.GetFileSpec().GetFilename() or "", entry.GetLine() + + +def frames(process: lldb.SBProcess) -> List[lldb.SBFrame]: + thread = process.GetSelectedThread() + return [thread.GetFrameAtIndex(index) for index in range(thread.GetNumFrames())] + + +def describe_process(process: lldb.SBProcess) -> str: + thread = process.GetSelectedThread() + lines = [ + f"state={lldb.SBDebugger.StateAsCString(process.GetState())} " + f"stop={thread.GetStopDescription(256)!r}" + ] + for index, frame in enumerate(frames(process)): + source, line = frame_location(frame) + lines.append(f"#{index} {frame_name(frame)} at {source}:{line}") + return "\n".join(lines) + + +def require_stopped(process: lldb.SBProcess, scenario: str) -> None: + if not process.IsValid() or process.GetState() != lldb.eStateStopped: + raise AcceptanceError(f"{scenario} did not stop:\n{describe_process(process)}") + + +def find_frame( + process: lldb.SBProcess, + function_suffix: str, + source_name: str, + source_line: int, +) -> int: + for index, frame in enumerate(frames(process)): + source, line = frame_location(frame) + if ( + frame_name(frame).endswith(function_suffix) + and source == source_name + and line == source_line + ): + return index + raise AcceptanceError( + f"missing {function_suffix} at {source_name}:{source_line}:\n" + f"{describe_process(process)}" + ) + + +def create_session(executable: str) -> tuple[lldb.SBDebugger, lldb.SBTarget]: + debugger = lldb.SBDebugger.Create() + debugger.SetAsync(False) + target = debugger.CreateTarget(executable) + if not target.IsValid(): + lldb.SBDebugger.Destroy(debugger) + raise AcceptanceError(f"could not create target for {executable}") + return debugger, target + + +def launch(target: lldb.SBTarget, scenario: str, cwd: str) -> lldb.SBProcess: + process = target.LaunchSimple([scenario], None, cwd) + require_stopped(process, scenario) + return process + + +def destroy_session(debugger: lldb.SBDebugger, process: lldb.SBProcess) -> None: + if process.IsValid() and process.GetState() not in ( + lldb.eStateExited, + lldb.eStateDetached, + ): + process.Kill() + lldb.SBDebugger.Destroy(debugger) + + +def set_panic_breakpoint(target: lldb.SBTarget) -> lldb.SBBreakpoint: + candidates: Iterable[str] = ( + "github.com/goplus/llgo/runtime/internal/runtime.Panic", + "runtime.Panic", + ) + for name in candidates: + breakpoint = target.BreakpointCreateByName(name) + if breakpoint.IsValid() and breakpoint.GetNumLocations() > 0: + return breakpoint + target.BreakpointDelete(breakpoint.GetID()) + breakpoint = target.BreakpointCreateByRegex( + r"(^|/)runtime(/internal/runtime)?\.Panic$" + ) + if not breakpoint.IsValid() or breakpoint.GetNumLocations() == 0: + raise AcceptanceError("could not resolve the LLGo runtime.Panic entry") + return breakpoint + + +def check_go_panic( + executable: str, + cwd: str, + scenario: str, + function: str, + line: int, +) -> None: + debugger, target = create_session(executable) + process = lldb.SBProcess() + try: + set_panic_breakpoint(target) + process = launch(target, scenario, cwd) + top = process.GetSelectedThread().GetFrameAtIndex(0) + if not frame_name(top).endswith("runtime.Panic"): + raise AcceptanceError( + f"{scenario} did not stop at the shared runtime.Panic entry:\n" + f"{describe_process(process)}" + ) + find_frame(process, function, "main.go", line) + finally: + destroy_session(debugger, process) + + +def check_trap(executable: str, cwd: str, root: Path) -> None: + debugger, target = create_session(executable) + process = lldb.SBProcess() + try: + process = launch(target, "trap", cwd) + stop = process.GetSelectedThread().GetStopReason() + if stop not in (lldb.eStopReasonSignal, lldb.eStopReasonException): + raise AcceptanceError( + f"trap stopped for reason {stop}, not a host exception:\n" + f"{describe_process(process)}" + ) + find_frame( + process, + "llgo_debug_trap", + "bridge.c", + marker_line(root / "bridge.c", "LLDB_STOP: host_trap"), + ) + find_frame( + process, + "main.hostTrap", + "main.go", + marker_line(root / "main.go", "LLDB_STOP: go_trap_caller"), + ) + finally: + destroy_session(debugger, process) + + +def check_boundary(executable: str, cwd: str, root: Path) -> None: + debugger, target = create_session(executable) + process = lldb.SBProcess() + try: + callback_line = marker_line(root / "main.go", "LLDB_STOP: go_callback") + breakpoint = target.BreakpointCreateByLocation("main.go", callback_line) + if not breakpoint.IsValid() or breakpoint.GetNumLocations() != 1: + raise AcceptanceError( + f"expected one Go callback breakpoint, found " + f"{breakpoint.GetNumLocations()}" + ) + process = launch(target, "boundary", cwd) + callback = find_frame( + process, "llgo_debug_go_callback", "main.go", callback_line + ) + host = find_frame( + process, + "llgo_debug_host_bridge", + "bridge.c", + marker_line(root / "bridge.c", "LLDB_STOP: host_callback"), + ) + caller = find_frame( + process, + "main.crossHostBoundary", + "main.go", + marker_line(root / "main.go", "LLDB_STOP: go_host_caller"), + ) + if not callback < host < caller: + raise AcceptanceError( + "Go/host/Go frames are not ordered across the boundary:\n" + f"{describe_process(process)}" + ) + finally: + destroy_session(debugger, process) + + +def check_optimized_inline(executable: str, cwd: str, root: Path) -> None: + debugger, target = create_session(executable) + process = lldb.SBProcess() + try: + leaf_line = marker_line(root / "inline.go", "LLDB_STOP: inline_leaf") + breakpoint = target.BreakpointCreateByLocation("inline.go", leaf_line) + if not breakpoint.IsValid() or breakpoint.GetNumLocations() == 0: + raise AcceptanceError("optimized inline leaf has no source breakpoint") + process = launch(target, "inline", cwd) + leaf = find_frame(process, "main.inlineLeaf", "inline.go", leaf_line) + middle = find_frame( + process, + "main.inlineMiddle", + "inline.go", + marker_line(root / "inline.go", "LLDB_STOP: inline_middle"), + ) + caller = find_frame( + process, + "main.optimizedInlineCaller", + "inline.go", + marker_line(root / "inline.go", "LLDB_STOP: inline_caller"), + ) + current_frames = frames(process) + if not leaf < middle < caller: + raise AcceptanceError( + "optimized inline frames are not nested in source order:\n" + f"{describe_process(process)}" + ) + if not current_frames[leaf].IsInlined() or not current_frames[middle].IsInlined(): + raise AcceptanceError( + "optimized leaf and middle are not represented as inline frames:\n" + f"{describe_process(process)}" + ) + if current_frames[caller].IsInlined(): + raise AcceptanceError( + "the noinline caller unexpectedly has only an inline frame:\n" + f"{describe_process(process)}" + ) + + after_line = marker_line(root / "inline.go", "LLDB_STOP: inline_after_call") + for _ in range(8): + process.GetSelectedThread().StepOver() + require_stopped(process, "optimized inline step-over") + top = process.GetSelectedThread().GetFrameAtIndex(0) + source, line = frame_location(top) + if ( + frame_name(top).endswith("main.optimizedInlineCaller") + and source == "inline.go" + and line == after_line + ): + break + else: + raise AcceptanceError( + "step-over did not return from inline code to its caller's next line:\n" + f"{describe_process(process)}" + ) + finally: + destroy_session(debugger, process) + + +def run_all(executable: str, optimized_executable: str, source_root: str) -> None: + root = Path(source_root).resolve() + cwd = str(root) + panic_cases = ( + ("panic", "main.explicitPanic", "LLDB_STOP: explicit_panic"), + ("divide", "main.divideByZero", "LLDB_STOP: divide_by_zero"), + ("invalid-memory", "main.invalidMemory", "LLDB_STOP: invalid_memory"), + ) + for scenario, function, marker in panic_cases: + check_go_panic( + executable, + cwd, + scenario, + function, + marker_line(root / "main.go", marker), + ) + check_trap(executable, cwd, root) + check_boundary(executable, cwd, root) + check_optimized_inline(optimized_executable, cwd, root) + print("NATIVE_DEBUG_ACCEPTANCE_OK") diff --git a/cmd/llgo/debugtest/native/bridge.c b/cmd/llgo/debugtest/native/bridge.c new file mode 100644 index 0000000000..d83caa7f60 --- /dev/null +++ b/cmd/llgo/debugtest/native/bridge.c @@ -0,0 +1,14 @@ +#include "bridge.h" + +extern int llgo_debug_go_callback(int value); + +__attribute__((noinline)) +int llgo_debug_host_bridge(int value) { + int result = llgo_debug_go_callback(value); /* LLDB_STOP: host_callback */ + return result + 1; +} + +__attribute__((noinline)) +void llgo_debug_trap(void) { + __builtin_trap(); /* LLDB_STOP: host_trap */ +} diff --git a/cmd/llgo/debugtest/native/bridge.h b/cmd/llgo/debugtest/native/bridge.h new file mode 100644 index 0000000000..8c7e34eabd --- /dev/null +++ b/cmd/llgo/debugtest/native/bridge.h @@ -0,0 +1,7 @@ +#ifndef LLGO_DEBUG_NATIVE_BRIDGE_H +#define LLGO_DEBUG_NATIVE_BRIDGE_H + +int llgo_debug_host_bridge(int value); +void llgo_debug_trap(void); + +#endif diff --git a/cmd/llgo/debugtest/native/go.mod b/cmd/llgo/debugtest/native/go.mod new file mode 100644 index 0000000000..0d6052626b --- /dev/null +++ b/cmd/llgo/debugtest/native/go.mod @@ -0,0 +1,3 @@ +module llgo-debug-native + +go 1.21 diff --git a/cmd/llgo/debugtest/native/inline.go b/cmd/llgo/debugtest/native/inline.go new file mode 100644 index 0000000000..70136ea728 --- /dev/null +++ b/cmd/llgo/debugtest/native/inline.go @@ -0,0 +1,26 @@ +package main + +var inlineObservation int + +//go:noinline +func inlineOpaque(value int) int { + inlineObservation = value + return value +} + +func inlineLeaf(value int) int { + adjusted := inlineOpaque(value) + 2 // LLDB_STOP: inline_leaf + return adjusted +} + +func inlineMiddle(value int) int { + doubled := inlineLeaf(value) * 2 // LLDB_STOP: inline_middle + return doubled +} + +//go:noinline +func optimizedInlineCaller(value int) int { + result := inlineMiddle(value) + 1 // LLDB_STOP: inline_caller + result++ // LLDB_STOP: inline_after_call + return result +} diff --git a/cmd/llgo/debugtest/native/main.go b/cmd/llgo/debugtest/native/main.go new file mode 100644 index 0000000000..7c7d8851e7 --- /dev/null +++ b/cmd/llgo/debugtest/native/main.go @@ -0,0 +1,66 @@ +package main + +/* +#include "bridge.h" +*/ +import "C" + +import "os" + +//go:noinline +func explicitPanic() { + panic("native debugger panic") // LLDB_STOP: explicit_panic +} + +// Keep the generated-check operation on the declaration line: debuggers may +// attribute the caller return PC to either side of the runtime helper call. +// +//go:noinline +func divideByZero(divisor int) int { return 42 / divisor } // LLDB_STOP: divide_by_zero + +//go:noinline +func invalidMemory(pointer *int) int { return *pointer } // LLDB_STOP: invalid_memory + +//go:noinline +func hostTrap() { + C.llgo_debug_trap() // LLDB_STOP: go_trap_caller +} + +//go:noinline +func crossHostBoundary(value int) int { + result := C.llgo_debug_host_bridge(C.int(value)) // LLDB_STOP: go_host_caller + return int(result) +} + +//go:noinline +//export llgo_debug_go_callback +func llgo_debug_go_callback(value C.int) C.int { + result := value + 1 // LLDB_STOP: go_callback + return result +} + +func main() { + if len(os.Args) != 2 { + panic("expected one debug scenario") + } + switch os.Args[1] { + case "panic": + explicitPanic() + case "divide": + println(divideByZero(len(os.Args) - 2)) + case "invalid-memory": + println(invalidMemory(nil)) + case "trap": + hostTrap() + case "boundary": + if got := crossHostBoundary(40); got != 42 { + panic("bad host boundary result") + } + case "inline": + if got := optimizedInlineCaller(len(os.Args) * 10); got != 46 { + panic("bad optimized inline result") + } + default: + panic("unknown debug scenario") + } +} diff --git a/cmd/llgo/debugtest/native/runtest.sh b/cmd/llgo/debugtest/native/runtest.sh new file mode 100755 index 0000000000..7d285b9c80 --- /dev/null +++ b/cmd/llgo/debugtest/native/runtest.sh @@ -0,0 +1,36 @@ +#!/bin/bash + +set -euo pipefail + +script_dir=$(cd "$(dirname "$0")" && pwd) + +# shellcheck source=../../lldbtest/common.sh +# shellcheck disable=SC1091 +source "$script_dir/../../lldbtest/common.sh" + +LLGO=${LLGO:-llgo} + +test_tmp_dir=$(mktemp -d "${TMPDIR:-/tmp}/llgo-native-debug.XXXXXX") +trap 'rm -rf "$test_tmp_dir"' EXIT +artifact="$test_tmp_dir/native-debug.out" +optimized_artifact="$test_tmp_dir/native-debug-o2.out" + +cd "$script_dir" +"$LLGO" build -O0 -ldflags=-w=false -o "$artifact" . +"$LLGO" build -O2 -ldflags=-w=false -o "$optimized_artifact" . + +lldb_output=$( + "$LLDB_PATH" --batch "$artifact" \ + -o "command script import \"$script_dir/acceptance.py\"" \ + -o "script acceptance.run_all(\"$artifact\", \"$optimized_artifact\", \"$script_dir\")" \ + 2>&1 +) || { + printf '%s\n' "$lldb_output" + exit 1 +} +printf '%s\n' "$lldb_output" + +if [[ "$lldb_output" == *"Traceback (most recent call last)"* ]] || \ + [[ "$lldb_output" != *"NATIVE_DEBUG_ACCEPTANCE_OK"* ]]; then + exit 1 +fi diff --git a/cmd/llgo/debugtest/wasi/runtest.sh b/cmd/llgo/debugtest/wasi/runtest.sh index 29ad58df2b..1b9f50b84b 100755 --- a/cmd/llgo/debugtest/wasi/runtest.sh +++ b/cmd/llgo/debugtest/wasi/runtest.sh @@ -20,6 +20,15 @@ export LLGO_ROOT="${repo_root}" export GOMEMLIMIT="${GOMEMLIMIT:-4GiB}" export GOMAXPROCS="${GOMAXPROCS:-4}" +dwarfdump="${LLGO_DWARFDUMP:-}" +if [[ -z "${dwarfdump}" ]]; then + dwarfdump="$(command -v llvm-dwarfdump || command -v llvm-dwarfdump-19 || true)" +fi +if [[ -z "${dwarfdump}" ]]; then + echo "llvm-dwarfdump is required to verify the final WASI artifact" >&2 + exit 2 +fi + (cd "${repo_root}" && go build -o "${tmp_dir}/llgo" ./cmd/llgo) ( @@ -43,7 +52,6 @@ grep -F '(int) result = 42' "${tmp_dir}/session.log" grep -F 'main.main at main.go:' "${tmp_dir}/session.log" grep -F 'Process 1 exited with status = 0' "${tmp_dir}/session.log" -if command -v llvm-dwarfdump >/dev/null 2>&1; then - llvm-dwarfdump --verify --error-display=quiet "${tmp_dir}/program.wasm" | tee "${tmp_dir}/dwarf.log" - grep -F 'No errors.' "${tmp_dir}/dwarf.log" -fi +"${dwarfdump}" --verify --error-display=quiet \ + "${tmp_dir}/program.wasm" 2>&1 | tee "${tmp_dir}/dwarf.log" +grep -Fx 'No errors.' "${tmp_dir}/dwarf.log" diff --git a/internal/build/build.go b/internal/build/build.go index 39a43279bd..b29706f99c 100644 --- a/internal/build/build.go +++ b/internal/build/build.go @@ -2583,6 +2583,7 @@ func clFiles(ctx *context, files string, pkg *packages.Package, procFile func(li func clFile(ctx *context, args []string, cFile, expFile, pkgPath string, procFile func(linkFile string), verbose bool) { baseName := expFile + filepath.Base(cFile) ext := filepath.Ext(cFile) + args = append(slices.Clone(args), dwarfCompilerArgs(ctx.buildConf, &ctx.crossCompile)...) // default clang++ will use c++ to compile c file,will cause symbol be mangled if ext == ".c" { diff --git a/internal/build/link_options.go b/internal/build/link_options.go index 68220b1c85..a16fe5ded0 100644 --- a/internal/build/link_options.go +++ b/internal/build/link_options.go @@ -141,3 +141,14 @@ func dwarfPreserveLinkerArgs(conf *Config, target *crosscompile.Export) []string } return slices.Clone(target.DebugInfo.PreserveLinkFlags) } + +// dwarfCompilerArgs keeps source information from C, C++, cgo preambles, and +// LLGoFiles under the same typed policy as Go-generated LLVM modules. Put the +// flag after package CFLAGS so a preserve decision cannot be undone by an +// earlier -g0. +func dwarfCompilerArgs(conf *Config, target *crosscompile.Export) []string { + if !shouldEmitDebugInfo(conf, target) { + return nil + } + return []string{"-gdwarf-4"} +} diff --git a/internal/build/link_options_test.go b/internal/build/link_options_test.go index 735bb1f630..c7206a310a 100644 --- a/internal/build/link_options_test.go +++ b/internal/build/link_options_test.go @@ -83,6 +83,29 @@ func TestDwarfPreserveLinkerArgs(t *testing.T) { } } +func TestDwarfCompilerArgs(t *testing.T) { + tests := []struct { + name string + conf Config + target crosscompile.Export + want []string + }{ + {name: "linked default", conf: Config{Mode: ModeBuild}, want: []string{"-gdwarf-4"}}, + {name: "explicit preserve", conf: Config{Mode: ModeBuild, LinkOptions: LinkOptions{DWARF: DWARFPreserve}}, target: configurableDebugInfo(), want: []string{"-gdwarf-4"}}, + {name: "explicit omit", conf: Config{Mode: ModeBuild, LinkOptions: LinkOptions{DWARF: DWARFOmit}}, target: configurableDebugInfo()}, + {name: "generation default", conf: Config{Mode: ModeGen}}, + {name: "generation preserve", conf: Config{Mode: ModeGen, LinkOptions: LinkOptions{DWARF: DWARFPreserve}}, want: []string{"-gdwarf-4"}}, + {name: "unsupported target", conf: Config{Mode: ModeBuild}, target: unavailableDebugInfo()}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := dwarfCompilerArgs(&tt.conf, &tt.target); !reflect.DeepEqual(got, tt.want) { + t.Fatalf("dwarfCompilerArgs() = %v, want %v", got, tt.want) + } + }) + } +} + func TestEffectiveOmitDWARF(t *testing.T) { tests := []struct { name string