commit
cfed8c150d
16 changed files with 960 additions and 0 deletions
@ -0,0 +1,102 @@ |
|||||||
|
# DAP Remote Debugging Example |
||||||
|
|
||||||
|
A minimal example showing how to debug a C++ program running in a **remote** |
||||||
|
container from any DAP-compatible client — Neovim, VSCode, Emacs, and others. |
||||||
|
|
||||||
|
``` |
||||||
|
┌──────────────────────┐ ┌──────────────────────┐ |
||||||
|
│ YOUR EDITOR │ DAP over TCP │ REMOTE │ |
||||||
|
│ (Neovim, VSCode,…) │ ◀──────────────────── ▶│ (Alpine container) │ |
||||||
|
│ │ port 13000 │ │ |
||||||
|
│ • source code │ │ • codelldb DAP │ |
||||||
|
│ • breakpoints │ │ server │ |
||||||
|
│ • variable inspection │ │ • debugapp (C++17) │ |
||||||
|
│ │ │ • socat forwarder │ |
||||||
|
└──────────────────────┘ └──────────────────────┘ |
||||||
|
``` |
||||||
|
|
||||||
|
The remote container runs [codelldb](https://github.com/vadimcn/codelldb) as a |
||||||
|
DAP (Debug Adapter Protocol) server. Your editor connects to it over TCP, |
||||||
|
sends a `launch` request, and codelldb starts the program **on the remote**. |
||||||
|
This avoids gdbserver's entry-point assembly issue entirely and keeps source |
||||||
|
paths clean. |
||||||
|
|
||||||
|
## Quick start (full setup) |
||||||
|
|
||||||
|
The included `docker-compose.yml` spins up both the remote container and a |
||||||
|
Neovim-based devstation: |
||||||
|
|
||||||
|
```bash |
||||||
|
docker compose build |
||||||
|
docker compose up -d remote # start the codelldb DAP server |
||||||
|
docker compose run --rm devstation # interactive Neovim session |
||||||
|
``` |
||||||
|
|
||||||
|
Inside Neovim, open `src/calculator.cpp`, press `<leader>db` on a line to set |
||||||
|
a breakpoint, then `<leader>dc` and select **"Remote launch (codelldb remote:13000)"**. |
||||||
|
|
||||||
|
See **[devstation/README.md](devstation/README.md)** for the full Neovim keybinding |
||||||
|
reference. |
||||||
|
|
||||||
|
## Using your own editor |
||||||
|
|
||||||
|
You don't need the devstation container. Build and run the remote alone, then |
||||||
|
connect from whatever editor you prefer: |
||||||
|
|
||||||
|
```bash |
||||||
|
docker build -t dap-debug-remote -f remote/Dockerfile . |
||||||
|
docker run -d --name remote \ |
||||||
|
-p 13000:13000 \ |
||||||
|
--cap-add SYS_PTRACE \ |
||||||
|
--security-opt seccomp=unconfined \ |
||||||
|
dap-debug-remote |
||||||
|
``` |
||||||
|
|
||||||
|
Then follow the guide for your editor in |
||||||
|
**[remote/README.md](remote/README.md)** — it covers VSCode, Neovim, Emacs, |
||||||
|
and generic DAP clients. |
||||||
|
|
||||||
|
## Repository layout |
||||||
|
|
||||||
|
``` |
||||||
|
. |
||||||
|
├── docker-compose.yml # Two-service compose (remote + devstation) |
||||||
|
├── README.md # This file |
||||||
|
│ |
||||||
|
├── common/ |
||||||
|
│ └── project/ # C++ demo project (shared by both containers) |
||||||
|
│ ├── CMakeLists.txt |
||||||
|
│ └── src/ |
||||||
|
│ |
||||||
|
├── remote/ |
||||||
|
│ ├── Dockerfile # Alpine + codelldb + socat + project build |
||||||
|
│ ├── entrypoint.sh # codelldb DAP server + socat forwarder loop |
||||||
|
│ └── README.md # Standalone usage + VSCode/other editor guides |
||||||
|
│ |
||||||
|
└── devstation/ |
||||||
|
├── Dockerfile # Alpine + Neovim (LazyVim) + nvim-dap |
||||||
|
├── README.md # Neovim keybindings and setup details |
||||||
|
├── nvim-config/ |
||||||
|
│ └── lua/plugins/ # LazyVim plugin specs (theme, lsp, dap) |
||||||
|
└── scripts/ |
||||||
|
└── precompile-treesitter.lua |
||||||
|
``` |
||||||
|
|
||||||
|
## How it works |
||||||
|
|
||||||
|
1. **codelldb** runs on the remote as a DAP server (`--port 13001 |
||||||
|
--multi-session`). It binds to `127.0.0.1` only, so **socat** forwards |
||||||
|
external connections from `0.0.0.0:13000` to codelldb's listener. |
||||||
|
2. Your editor connects to port 13000 and sends a standard DAP `launch` |
||||||
|
request. codelldb starts the program on the remote, captures stdout/stderr, |
||||||
|
and streams output events back. |
||||||
|
3. The project source lives at `/project` on the remote (and on the devstation |
||||||
|
if you use it), so debug-info paths match and editors open source files |
||||||
|
correctly when stopped at a breakpoint. |
||||||
|
4. The remote entrypoint restarts codelldb after each session, so you can |
||||||
|
reconnect without restarting the container. |
||||||
|
5. `SYS_PTRACE` + `seccomp:unconfined` on the remote allow codelldb to debug |
||||||
|
the child process. |
||||||
|
|
||||||
|
> **Note:** codelldb is a glibc binary but runs on Alpine (musl) via `gcompat` |
||||||
|
> plus a tiny shim library providing the missing `__res_init` symbol. |
||||||
@ -0,0 +1,26 @@ |
|||||||
|
# Demo project |
||||||
|
|
||||||
|
A small C++17 calculator used as the debug target for this example. The same |
||||||
|
source code is copied into both the **remote** container (where it is compiled |
||||||
|
and debugged) and the **devstation** container (where you edit and browse it). |
||||||
|
|
||||||
|
## Build |
||||||
|
|
||||||
|
```bash |
||||||
|
cmake -DCMAKE_BUILD_TYPE=Debug -S . -B build |
||||||
|
cmake --build build -j |
||||||
|
``` |
||||||
|
|
||||||
|
This produces `build/debugapp` compiled with `-g3 -O0` (full debug info, zero |
||||||
|
optimisation) and generates `compile_commands.json` for clangd. |
||||||
|
|
||||||
|
## Good debugging targets |
||||||
|
|
||||||
|
| Feature | Where to try it | |
||||||
|
|---|---| |
||||||
|
| **Breakpoints** | Any method in `Calculator` — e.g. `Calculator::divide` | |
||||||
|
| **Conditional breakpoint** | In `runDemo()` at the `for` loop, set condition `i == 3` | |
||||||
|
| **Variable inspection** | `m_accumulator`, `m_history`, `m_callCount` | |
||||||
|
| **Watch expressions** | `m_history.size()`, `sum + diff` | |
||||||
|
| **Call stack** | Step into `formatResult()` to see the stack frames | |
||||||
|
| **Step over / into / out** | The `runDemo()` loop exercises all three | |
||||||
@ -0,0 +1,20 @@ |
|||||||
|
cmake_minimum_required(VERSION 3.16) |
||||||
|
project(dap_debug_example CXX) |
||||||
|
|
||||||
|
set(CMAKE_CXX_STANDARD 17) |
||||||
|
set(CMAKE_CXX_STANDARD_REQUIRED ON) |
||||||
|
set(CMAKE_CXX_EXTENSIONS OFF) |
||||||
|
|
||||||
|
# Full debug info, zero optimisation — ideal for stepping and inspection. |
||||||
|
set(CMAKE_CXX_FLAGS_DEBUG "-g3 -O0") |
||||||
|
|
||||||
|
add_executable(debugapp |
||||||
|
src/main.cpp |
||||||
|
src/calculator.cpp |
||||||
|
) |
||||||
|
|
||||||
|
target_include_directories(debugapp PRIVATE src) |
||||||
|
target_compile_options(debugapp PRIVATE -g3 -O0 -Wall -Wextra) |
||||||
|
|
||||||
|
# Generate compile_commands.json for clangd/LSP integration. |
||||||
|
set(CMAKE_EXPORT_COMPILE_COMMANDS ON) |
||||||
@ -0,0 +1,88 @@ |
|||||||
|
#include "calculator.h" |
||||||
|
|
||||||
|
#include <iostream> |
||||||
|
#include <iomanip> |
||||||
|
#include <sstream> |
||||||
|
|
||||||
|
Calculator::Calculator() |
||||||
|
: m_accumulator(0), m_callCount(0) {} |
||||||
|
|
||||||
|
int Calculator::add(int a, int b) { |
||||||
|
m_callCount++; |
||||||
|
int result = a + b; |
||||||
|
m_history.push_back(result); |
||||||
|
return result; |
||||||
|
} |
||||||
|
|
||||||
|
int Calculator::subtract(int a, int b) { |
||||||
|
m_callCount++; |
||||||
|
int result = a - b; |
||||||
|
m_history.push_back(result); |
||||||
|
return result; |
||||||
|
} |
||||||
|
|
||||||
|
int Calculator::multiply(int a, int b) { |
||||||
|
m_callCount++; |
||||||
|
int result = a * b; |
||||||
|
m_history.push_back(result); |
||||||
|
return result; |
||||||
|
} |
||||||
|
|
||||||
|
double Calculator::divide(int a, int b) { |
||||||
|
m_callCount++; |
||||||
|
if (b == 0) { |
||||||
|
log("Warning: division by zero!"); |
||||||
|
return 0.0; |
||||||
|
} |
||||||
|
double result = static_cast<double>(a) / static_cast<double>(b); |
||||||
|
m_history.push_back(static_cast<int>(result)); |
||||||
|
return result; |
||||||
|
} |
||||||
|
|
||||||
|
void Calculator::accumulate(int value) { |
||||||
|
m_accumulator += value; |
||||||
|
m_history.push_back(value); |
||||||
|
} |
||||||
|
|
||||||
|
int Calculator::getAccumulator() const { |
||||||
|
return m_accumulator; |
||||||
|
} |
||||||
|
|
||||||
|
void Calculator::log(const std::string& msg) { |
||||||
|
std::cerr << "[Calculator] " << msg << std::endl; |
||||||
|
} |
||||||
|
|
||||||
|
void Calculator::runDemo() { |
||||||
|
log("Starting demo..."); |
||||||
|
|
||||||
|
for (int i = 1; i <= 5; ++i) { |
||||||
|
// Good place for a conditional breakpoint, e.g.: i == 3
|
||||||
|
int sum = add(i, i * 2); |
||||||
|
accumulate(sum); |
||||||
|
|
||||||
|
int diff = subtract(sum, i); |
||||||
|
int product = multiply(diff, i); |
||||||
|
|
||||||
|
if (product > 0) { |
||||||
|
double quotient = divide(product, i); |
||||||
|
std::string formatted = formatResult("divide", product, i, quotient); |
||||||
|
std::cout << "Iteration " << i << ": " |
||||||
|
<< "sum=" << sum << ", " |
||||||
|
<< "diff=" << diff << ", " |
||||||
|
<< "product=" << product << ", " |
||||||
|
<< formatted |
||||||
|
<< std::endl; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
log("Demo complete. Accumulator = " + std::to_string(m_accumulator)); |
||||||
|
log("Total calls = " + std::to_string(m_callCount)); |
||||||
|
log("History size = " + std::to_string(m_history.size())); |
||||||
|
} |
||||||
|
|
||||||
|
std::string formatResult(const std::string& op, int a, int b, double result) { |
||||||
|
std::ostringstream oss; |
||||||
|
oss << op << "(" << a << ", " << b << ") = " |
||||||
|
<< std::fixed << std::setprecision(2) << result; |
||||||
|
return oss.str(); |
||||||
|
} |
||||||
@ -0,0 +1,34 @@ |
|||||||
|
#pragma once |
||||||
|
|
||||||
|
#include <string> |
||||||
|
#include <vector> |
||||||
|
|
||||||
|
/// A small calculator class designed to demonstrate DAP debugging features:
|
||||||
|
/// stepping, variable inspection, call stack navigation, conditional
|
||||||
|
/// breakpoints, and watch expressions.
|
||||||
|
class Calculator { |
||||||
|
public: |
||||||
|
Calculator(); |
||||||
|
|
||||||
|
int add(int a, int b); |
||||||
|
int subtract(int a, int b); |
||||||
|
int multiply(int a, int b); |
||||||
|
double divide(int a, int b); |
||||||
|
|
||||||
|
void accumulate(int value); |
||||||
|
int getAccumulator() const; |
||||||
|
int getCallCount() const { return m_callCount; } |
||||||
|
|
||||||
|
/// Runs a short interactive demo loop — the main target for debugging.
|
||||||
|
void runDemo(); |
||||||
|
|
||||||
|
private: |
||||||
|
int m_accumulator; |
||||||
|
int m_callCount; |
||||||
|
std::vector<int> m_history; |
||||||
|
|
||||||
|
void log(const std::string& msg); |
||||||
|
}; |
||||||
|
|
||||||
|
/// Free helper function — good for demonstrating the "step out" action.
|
||||||
|
std::string formatResult(const std::string& op, int a, int b, double result); |
||||||
@ -0,0 +1,19 @@ |
|||||||
|
#include "calculator.h" |
||||||
|
|
||||||
|
#include <iostream> |
||||||
|
|
||||||
|
int main() { |
||||||
|
std::cout << "=== DAP Debugging Example ===" << std::endl; |
||||||
|
std::cout << "Set breakpoints in calculator.cpp, then press <leader>dc." << std::endl; |
||||||
|
std::cout << std::endl; |
||||||
|
|
||||||
|
Calculator calc; |
||||||
|
calc.runDemo(); |
||||||
|
|
||||||
|
std::cout << std::endl; |
||||||
|
std::cout << "Final accumulator value: " << calc.getAccumulator() << std::endl; |
||||||
|
std::cout << "Total function calls: " << calc.getCallCount() << std::endl; |
||||||
|
std::cout << "Program completed successfully." << std::endl; |
||||||
|
|
||||||
|
return 0; |
||||||
|
} |
||||||
@ -0,0 +1,92 @@ |
|||||||
|
# syntax=docker/dockerfile:1 |
||||||
|
# |
||||||
|
# Developer station: Neovim (LazyVim) with nvim-dap configured to connect |
||||||
|
# to a codelldb DAP server running on the "remote" container. |
||||||
|
# |
||||||
|
# Builds the newest Neovim from source (Alpine uses musl, so prebuilt |
||||||
|
# glibc binaries don't run here) and layers on LazyVim, Catppuccin, and |
||||||
|
# the nvim-dap plugin stack. codelldb itself lives on the remote container. |
||||||
|
# |
||||||
|
# Build (run from repository root): |
||||||
|
# docker build -t dap-debug-dev -f devstation/Dockerfile . |
||||||
|
# |
||||||
|
# Pin a specific Neovim release: |
||||||
|
# docker build --build-arg NVIM_REF=v0.12.3 -t dap-debug-dev -f devstation/Dockerfile . |
||||||
|
|
||||||
|
############################################################################### |
||||||
|
# Stage 1 - build the newest Neovim from source |
||||||
|
############################################################################### |
||||||
|
FROM alpine:latest AS nvim-builder |
||||||
|
|
||||||
|
ARG NVIM_REF=stable |
||||||
|
|
||||||
|
RUN apk add --no-cache \ |
||||||
|
build-base cmake coreutils curl gettext-tiny-dev git \ |
||||||
|
linux-headers ninja unzip \ |
||||||
|
&& git clone --depth 1 --branch "${NVIM_REF}" https://github.com/neovim/neovim.git /tmp/neovim \ |
||||||
|
&& make -C /tmp/neovim \ |
||||||
|
CMAKE_BUILD_TYPE=Release \ |
||||||
|
CMAKE_EXTRA_FLAGS="-DCMAKE_INSTALL_PREFIX=/opt/nvim" \ |
||||||
|
&& make -C /tmp/neovim install \ |
||||||
|
&& strip /opt/nvim/bin/nvim |
||||||
|
|
||||||
|
############################################################################### |
||||||
|
# Stage 2 - runtime image with LazyVim and DAP configuration |
||||||
|
############################################################################### |
||||||
|
FROM alpine:latest |
||||||
|
|
||||||
|
ENV HOME=/home/dev \ |
||||||
|
TERM=xterm-256color \ |
||||||
|
PATH="/opt/nvim/bin:${PATH}" |
||||||
|
|
||||||
|
RUN addgroup -g 1000 dev \ |
||||||
|
&& adduser -D -u 1000 -G dev -h /home/dev -s /bin/ash dev |
||||||
|
|
||||||
|
# LazyVim runtime dependencies + C++ toolchain for reading/browsing code. |
||||||
|
# clang-extra-tools provides clangd for LSP code navigation. |
||||||
|
RUN apk add --no-cache \ |
||||||
|
git curl ca-certificates \ |
||||||
|
ripgrep fd fzf \ |
||||||
|
build-base cmake ccmake \ |
||||||
|
gdb strace \ |
||||||
|
clang-extra-tools \ |
||||||
|
bash unzip less \ |
||||||
|
ncurses-terminfo \ |
||||||
|
tree-sitter-cli |
||||||
|
|
||||||
|
# Neovim built in stage 1. |
||||||
|
COPY --from=nvim-builder /opt/nvim /opt/nvim |
||||||
|
|
||||||
|
# C++ demo project — source for editing, browsing, and setting breakpoints. |
||||||
|
COPY --chown=dev:dev common/project/ /project/ |
||||||
|
|
||||||
|
USER dev |
||||||
|
WORKDIR /project |
||||||
|
|
||||||
|
RUN mkdir -p build \ |
||||||
|
&& cd build \ |
||||||
|
&& cmake -DCMAKE_BUILD_TYPE=Debug .. \ |
||||||
|
&& make -j"$(nproc)" \ |
||||||
|
&& ln -sf /project/build/compile_commands.json /project/compile_commands.json |
||||||
|
|
||||||
|
# --------------------------------------------------------------------------- |
||||||
|
# LazyVim configuration |
||||||
|
# --------------------------------------------------------------------------- |
||||||
|
|
||||||
|
# LazyVim starter configuration (https://github.com/LazyVim/starter). |
||||||
|
RUN git clone --depth 1 https://github.com/LazyVim/starter "${HOME}/.config/nvim" \ |
||||||
|
&& rm -rf "${HOME}/.config/nvim/.git" |
||||||
|
|
||||||
|
# Plugin specs — theme, LSP, and DAP configuration. |
||||||
|
# See devstation/nvim-config/lua/plugins/ for the source files. |
||||||
|
COPY --chown=dev:dev devstation/nvim-config/lua/plugins/ "${HOME}/.config/nvim/lua/plugins/" |
||||||
|
|
||||||
|
# Pre-install plugins so the editor is ready to use on the first launch. |
||||||
|
RUN timeout 600 nvim --headless "+Lazy! sync" +qa || true |
||||||
|
|
||||||
|
# Pre-compile LazyVim's treesitter parsers. |
||||||
|
COPY --chown=dev:dev devstation/scripts/precompile-treesitter.lua /tmp/precompile-treesitter.lua |
||||||
|
RUN timeout 900 nvim --headless -c "luafile /tmp/precompile-treesitter.lua" \ |
||||||
|
&& rm -f /tmp/precompile-treesitter.lua |
||||||
|
|
||||||
|
CMD ["nvim"] |
||||||
@ -0,0 +1,87 @@ |
|||||||
|
# Devstation container |
||||||
|
|
||||||
|
An Alpine container with Neovim (LazyVim) pre-configured for remote C++ DAP |
||||||
|
debugging. It connects to the codelldb DAP server running on the **remote** |
||||||
|
container. |
||||||
|
|
||||||
|
## What's inside |
||||||
|
|
||||||
|
| Component | Purpose | |
||||||
|
|---|---| |
||||||
|
| Neovim (built from source) | Editor — Alpine uses musl, so prebuilt glibc binaries don't run | |
||||||
|
| [LazyVim](https://lazyvim.org) | Neovim distribution (pre-installed) | |
||||||
|
| Catppuccin (Mocha) | Colour scheme | |
||||||
|
| nvim-dap + dap-ui + virtual-text | DAP client, UI panels, inline variable text | |
||||||
|
| clangd (via `clang-extra-tools`) | LSP for C/C++ code navigation | |
||||||
|
| ripgrep, fd, fzf | Fuzzy finder and search backends | |
||||||
|
|
||||||
|
## Quick start |
||||||
|
|
||||||
|
```bash |
||||||
|
# From the repository root: |
||||||
|
docker compose build |
||||||
|
docker compose up -d remote |
||||||
|
docker compose run --rm devstation |
||||||
|
``` |
||||||
|
|
||||||
|
Neovim launches automatically in `/project`. |
||||||
|
|
||||||
|
## Debugging |
||||||
|
|
||||||
|
1. Open a source file: `:e src/calculator.cpp` |
||||||
|
2. Set a breakpoint: `<leader>db` |
||||||
|
3. Start debugging: `<leader>dc` |
||||||
|
4. Select **"Remote launch (codelldb remote:13000)"** |
||||||
|
5. The program runs in the remote container and stops at your breakpoint. |
||||||
|
6. dap-ui opens automatically with scopes, watch, call stack, and breakpoints. |
||||||
|
|
||||||
|
## Key bindings |
||||||
|
|
||||||
|
All debug keybindings are under the `<leader>d` prefix: |
||||||
|
|
||||||
|
| Key | Action | |
||||||
|
|---|---| |
||||||
|
| `<leader>db` | Toggle breakpoint | |
||||||
|
| `<leader>dB` | Conditional breakpoint | |
||||||
|
| `<leader>dc` | Continue / start debug session | |
||||||
|
| `<leader>dC` | Run to cursor | |
||||||
|
| `<leader>di` | Step into | |
||||||
|
| `<leader>dO` | Step over | |
||||||
|
| `<leader>do` | Step out | |
||||||
|
| `<leader>dp` | Pause | |
||||||
|
| `<leader>dt` | Terminate session | |
||||||
|
| `<leader>dr` | Toggle REPL | |
||||||
|
| `<leader>du` | Toggle DAP UI | |
||||||
|
| `<leader>de` | Evaluate expression (normal or visual mode) | |
||||||
|
| `<leader>dw` | Hover (inspect variable under cursor) | |
||||||
|
| `<leader>dj` / `<leader>dk` | Navigate call stack (down / up) | |
||||||
|
| `<leader>dl` | Run last session | |
||||||
|
| `<leader>dg` | Go to line (skip execution) | |
||||||
|
| `<leader>ds` | Session info | |
||||||
|
|
||||||
|
## Plugin configuration |
||||||
|
|
||||||
|
LazyVim plugin specs live as regular files under |
||||||
|
[`devstation/nvim-config/lua/plugins/`](nvim-config/lua/plugins/) and are |
||||||
|
COPY'd into the image at build time: |
||||||
|
|
||||||
|
| File | Contents | |
||||||
|
|---|---| |
||||||
|
| `theme.lua` | Catppuccin Mocha theme | |
||||||
|
| `lsp.lua` | Disables Mason auto-install (glibc binaries crash on musl); uses system clangd | |
||||||
|
| `dap.lua` | nvim-dap adapter (connects to `remote:13000`) + debug config + all `<leader>d` keybindings | |
||||||
|
|
||||||
|
The treesitter pre-compilation script is at |
||||||
|
[`devstation/scripts/precompile-treesitter.lua`](scripts/precompile-treesitter.lua). |
||||||
|
|
||||||
|
To customise the DAP adapter (e.g. change host or port), edit the |
||||||
|
`dap.adapters.codelldb` table in `dap.lua` and rebuild. |
||||||
|
|
||||||
|
## Notes |
||||||
|
|
||||||
|
- Named volumes (`nvim-data`, `nvim-state`, `nvim-cache`) persist plugins and |
||||||
|
state across container restarts. Remove them with |
||||||
|
`docker compose down -v` to start fresh. |
||||||
|
- Mason's prebuilt binaries are glibc-linked and crash on Alpine/musl. LSP |
||||||
|
servers are provided by system packages instead (`clang-extra-tools`). |
||||||
|
- Treesitter parsers are pre-compiled during the image build. |
||||||
@ -0,0 +1,69 @@ |
|||||||
|
-- DAP plugin stack: core client, UI, inline virtual text. |
||||||
|
-- Provides <leader>d keybindings and codelldb adapter configuration. |
||||||
|
return { |
||||||
|
{ |
||||||
|
"mfussenegger/nvim-dap", |
||||||
|
dependencies = { |
||||||
|
"rcarriga/nvim-dap-ui", |
||||||
|
"theHamsta/nvim-dap-virtual-text", |
||||||
|
"nvim-neotest/nvim-nio", |
||||||
|
}, |
||||||
|
keys = { |
||||||
|
{ "<leader>d", "", desc = "+debug", mode = { "n", "v" } }, |
||||||
|
{ "<leader>db", function() require("dap").toggle_breakpoint() end, desc = "Toggle Breakpoint" }, |
||||||
|
{ "<leader>dB", function() require("dap").set_breakpoint(vim.fn.input("Breakpoint condition: ")) end, desc = "Conditional Breakpoint" }, |
||||||
|
{ "<leader>dc", function() require("dap").continue() end, desc = "Continue / Start" }, |
||||||
|
{ "<leader>dC", function() require("dap").run_to_cursor() end, desc = "Run to Cursor" }, |
||||||
|
{ "<leader>dg", function() require("dap").goto_() end, desc = "Go to Line (skip execution)" }, |
||||||
|
{ "<leader>di", function() require("dap").step_into() end, desc = "Step Into" }, |
||||||
|
{ "<leader>dj", function() require("dap").down() end, desc = "Frame Down" }, |
||||||
|
{ "<leader>dk", function() require("dap").up() end, desc = "Frame Up" }, |
||||||
|
{ "<leader>dl", function() require("dap").run_last() end, desc = "Run Last" }, |
||||||
|
{ "<leader>do", function() require("dap").step_out() end, desc = "Step Out" }, |
||||||
|
{ "<leader>dO", function() require("dap").step_over() end, desc = "Step Over" }, |
||||||
|
{ "<leader>dp", function() require("dap").pause() end, desc = "Pause" }, |
||||||
|
{ "<leader>dr", function() require("dap").repl.toggle() end, desc = "Toggle REPL" }, |
||||||
|
{ "<leader>ds", function() require("dap").session() end, desc = "Session Info" }, |
||||||
|
{ "<leader>dt", function() require("dap").terminate() end, desc = "Terminate" }, |
||||||
|
{ "<leader>dw", function() require("dap.ui.widgets").hover() end, desc = "Hover Widget" }, |
||||||
|
{ "<leader>du", function() require("dapui").toggle() end, desc = "Toggle DAP UI" }, |
||||||
|
{ "<leader>de", function() require("dapui").eval() end, desc = "Evaluate Expression", mode = { "n", "v" } }, |
||||||
|
}, |
||||||
|
config = function() |
||||||
|
local dap = require("dap") |
||||||
|
local dapui = require("dapui") |
||||||
|
|
||||||
|
dapui.setup() |
||||||
|
dap.listeners.after.event_initialized["dapui_config"] = function() dapui.open() end |
||||||
|
dap.listeners.before.event_terminated["dapui_config"] = function() dapui.close() end |
||||||
|
dap.listeners.before.event_exited["dapui_config"] = function() dapui.close() end |
||||||
|
|
||||||
|
require("nvim-dap-virtual-text").setup() |
||||||
|
|
||||||
|
-- Adapter: connect to codelldb running on the "remote" container. |
||||||
|
-- codelldb listens as a DAP server on port 13000. |
||||||
|
dap.adapters.codelldb = { |
||||||
|
type = "server", |
||||||
|
host = "remote", |
||||||
|
port = 13000, |
||||||
|
} |
||||||
|
|
||||||
|
-- Debug configurations. |
||||||
|
dap.configurations.cpp = { |
||||||
|
{ |
||||||
|
-- Remote launch: codelldb on the remote starts the program. |
||||||
|
-- Source paths in debug info (/project/src/...) match the |
||||||
|
-- devstation's filesystem, so nvim-dap opens source files |
||||||
|
-- correctly when stopped at a breakpoint. |
||||||
|
name = "Remote launch (codelldb remote:13000)", |
||||||
|
type = "codelldb", |
||||||
|
request = "launch", |
||||||
|
program = "/project/build/debugapp", |
||||||
|
cwd = "/project", |
||||||
|
stopOnEntry = false, |
||||||
|
}, |
||||||
|
} |
||||||
|
dap.configurations.c = dap.configurations.cpp |
||||||
|
end, |
||||||
|
}, |
||||||
|
} |
||||||
@ -0,0 +1,20 @@ |
|||||||
|
return { |
||||||
|
{ |
||||||
|
"mason-org/mason.nvim", |
||||||
|
opts = { |
||||||
|
PATH = "append", |
||||||
|
ensure_installed = {}, |
||||||
|
}, |
||||||
|
}, |
||||||
|
{ |
||||||
|
"neovim/nvim-lspconfig", |
||||||
|
opts = { |
||||||
|
servers = { |
||||||
|
-- Prevent Mason from auto-installing these (glibc binaries crash |
||||||
|
-- on Alpine/musl). clangd is provided by the system package. |
||||||
|
lua_ls = { mason = false }, |
||||||
|
clangd = { mason = false }, |
||||||
|
}, |
||||||
|
}, |
||||||
|
}, |
||||||
|
} |
||||||
@ -0,0 +1,9 @@ |
|||||||
|
return { |
||||||
|
{ |
||||||
|
"catppuccin/nvim", |
||||||
|
name = "catppuccin", |
||||||
|
priority = 1000, |
||||||
|
opts = { flavour = "mocha" }, |
||||||
|
}, |
||||||
|
{ "LazyVim/LazyVim", opts = { colorscheme = "catppuccin" } }, |
||||||
|
} |
||||||
@ -0,0 +1,32 @@ |
|||||||
|
local lazy = require("lazy") |
||||||
|
local Plugin = require("lazy.core.plugin") |
||||||
|
|
||||||
|
local dir, ts |
||||||
|
for _, p in ipairs(lazy.plugins()) do |
||||||
|
if p.name == "nvim-treesitter" then |
||||||
|
dir, ts = p.dir, p |
||||||
|
break |
||||||
|
end |
||||||
|
end |
||||||
|
|
||||||
|
vim.opt.runtimepath:append(dir) |
||||||
|
local ensure = Plugin.values(ts, "opts", false).ensure_installed or {} |
||||||
|
|
||||||
|
local TS = require("nvim-treesitter") |
||||||
|
local installed = TS.get_installed("parsers") |
||||||
|
local missing = vim.tbl_filter(function(l) |
||||||
|
return not vim.list_contains(installed, l) |
||||||
|
end, ensure) |
||||||
|
|
||||||
|
if #missing > 0 then |
||||||
|
print("[precompile] installing treesitter parsers: " .. table.concat(missing, ", ")) |
||||||
|
local ok, err = pcall(function() |
||||||
|
TS.install(missing, { summary = true }):wait() |
||||||
|
end) |
||||||
|
if not ok then |
||||||
|
print("[precompile] warning: " .. tostring(err)) |
||||||
|
end |
||||||
|
else |
||||||
|
print("[precompile] all ensure_installed parsers already present") |
||||||
|
end |
||||||
|
vim.cmd("qa") |
||||||
@ -0,0 +1,63 @@ |
|||||||
|
# DAP Debugging Example |
||||||
|
# |
||||||
|
# Two-container setup for remote C++ debugging with Neovim DAP: |
||||||
|
# |
||||||
|
# "remote" – Alpine container running codelldb as a DAP server. |
||||||
|
# "devstation" – Alpine container with Neovim + LazyVim + nvim-dap. |
||||||
|
# |
||||||
|
# Usage: |
||||||
|
# docker compose build |
||||||
|
# docker compose up -d remote # start codelldb DAP server |
||||||
|
# docker compose run --rm devstation # interactive Neovim session |
||||||
|
# |
||||||
|
# Plugins, caches, and state persist in named volumes across runs, so |
||||||
|
# Neovim does not re-download plugins on every container start. |
||||||
|
# |
||||||
|
# Inside Neovim, open a source file (e.g. src/calculator.cpp), set a |
||||||
|
# breakpoint with <leader>db, then press <leader>dc and select |
||||||
|
# "Remote launch (codelldb remote:13000)". |
||||||
|
|
||||||
|
services: |
||||||
|
remote: |
||||||
|
build: |
||||||
|
context: . |
||||||
|
dockerfile: remote/Dockerfile |
||||||
|
ports: |
||||||
|
- "13000:13000" |
||||||
|
networks: |
||||||
|
- debug-net |
||||||
|
restart: unless-stopped |
||||||
|
# SYS_PTRACE + seccomp:unconfined let codelldb control the child |
||||||
|
# process (set breakpoints, read memory, single-step, etc.). |
||||||
|
cap_add: |
||||||
|
- SYS_PTRACE |
||||||
|
security_opt: |
||||||
|
- seccomp:unconfined |
||||||
|
|
||||||
|
devstation: |
||||||
|
build: |
||||||
|
context: . |
||||||
|
dockerfile: devstation/Dockerfile |
||||||
|
stdin_open: true |
||||||
|
tty: true |
||||||
|
working_dir: /project |
||||||
|
networks: |
||||||
|
- debug-net |
||||||
|
depends_on: |
||||||
|
- remote |
||||||
|
# Named volumes so Neovim plugins, caches, and state survive |
||||||
|
# container removal. Without these, every `docker compose run` |
||||||
|
# starts from scratch and re-downloads everything. |
||||||
|
volumes: |
||||||
|
- nvim-data:/home/dev/.local/share/nvim |
||||||
|
- nvim-state:/home/dev/.local/state/nvim |
||||||
|
- nvim-cache:/home/dev/.cache/nvim |
||||||
|
|
||||||
|
networks: |
||||||
|
debug-net: |
||||||
|
driver: bridge |
||||||
|
|
||||||
|
volumes: |
||||||
|
nvim-data: |
||||||
|
nvim-state: |
||||||
|
nvim-cache: |
||||||
@ -0,0 +1,54 @@ |
|||||||
|
# syntax=docker/dockerfile:1 |
||||||
|
# |
||||||
|
# Remote machine: runs codelldb as a DAP server. The developer station's |
||||||
|
# nvim-dap connects to it over TCP and sends a standard DAP "launch" |
||||||
|
# request — codelldb starts the program locally, so there is no |
||||||
|
# gdbserver entry-point assembly issue and source paths resolve cleanly. |
||||||
|
# |
||||||
|
# Build (run from repository root): |
||||||
|
# docker build -t dap-debug-remote -f remote/Dockerfile . |
||||||
|
|
||||||
|
FROM alpine:latest |
||||||
|
|
||||||
|
ARG CODELLDB_VERSION=1.12.2 |
||||||
|
|
||||||
|
RUN apk add --no-cache \ |
||||||
|
build-base cmake gdb \ |
||||||
|
curl unzip gcompat socat |
||||||
|
|
||||||
|
WORKDIR /project |
||||||
|
COPY common/project/ /project/ |
||||||
|
|
||||||
|
# Build with full debug info for a meaningful debugging experience. |
||||||
|
RUN mkdir -p build \ |
||||||
|
&& cd build \ |
||||||
|
&& cmake -DCMAKE_BUILD_TYPE=Debug .. \ |
||||||
|
&& make -j"$(nproc)" |
||||||
|
|
||||||
|
# codelldb DAP adapter — a glibc binary; gcompat provides the compatibility |
||||||
|
# layer so it runs on musl/Alpine. |
||||||
|
RUN mkdir -p /opt/codelldb \ |
||||||
|
&& curl -fsSL "https://github.com/vadimcn/codelldb/releases/download/v${CODELLDB_VERSION}/codelldb-linux-x64.vsix" \ |
||||||
|
-o /tmp/codelldb.vsix \ |
||||||
|
&& cd /opt/codelldb && unzip -q /tmp/codelldb.vsix \ |
||||||
|
&& rm /tmp/codelldb.vsix \ |
||||||
|
&& chmod +x /opt/codelldb/extension/adapter/codelldb |
||||||
|
|
||||||
|
# codelldb needs __res_init / __res_ninit (glibc DNS resolver init) which |
||||||
|
# gcompat does not provide. A tiny no-op shim fills the gap, then a wrapper |
||||||
|
# script applies LD_PRELOAD transparently. |
||||||
|
RUN printf 'int __res_init(void) { return 0; }\nint __res_ninit(void *s) { (void)s; return 0; }\n' \ |
||||||
|
> /tmp/resolv_shim.c \ |
||||||
|
&& gcc -shared -fPIC -o /usr/lib/libresolv_compat.so /tmp/resolv_shim.c \ |
||||||
|
&& rm /tmp/resolv_shim.c \ |
||||||
|
&& mv /opt/codelldb/extension/adapter/codelldb \ |
||||||
|
/opt/codelldb/extension/adapter/codelldb.bin \ |
||||||
|
&& printf '#!/bin/sh\nLD_PRELOAD=/usr/lib/libresolv_compat.so exec /opt/codelldb/extension/adapter/codelldb.bin "$@"\n' \ |
||||||
|
> /opt/codelldb/extension/adapter/codelldb \ |
||||||
|
&& chmod +x /opt/codelldb/extension/adapter/codelldb |
||||||
|
|
||||||
|
COPY remote/entrypoint.sh /entrypoint.sh |
||||||
|
|
||||||
|
EXPOSE 13000 |
||||||
|
|
||||||
|
ENTRYPOINT ["/entrypoint.sh"] |
||||||
@ -0,0 +1,207 @@ |
|||||||
|
# Remote debugging container |
||||||
|
|
||||||
|
An Alpine container that builds a C++ demo project and runs |
||||||
|
[codelldb](https://github.com/vadimcn/codelldb) as a DAP (Debug Adapter |
||||||
|
Protocol) server on **port 13000**. Any DAP-compatible editor can connect to |
||||||
|
it, set breakpoints, and debug the program running inside the container. |
||||||
|
|
||||||
|
``` |
||||||
|
YOUR EDITOR ──DAP over TCP:13000──▶ REMOTE CONTAINER |
||||||
|
├── codelldb (DAP server, 127.0.0.1:13001) |
||||||
|
├── socat (forwarder, 0.0.0.0:13000 ─▶ 13001) |
||||||
|
└── /project/build/debugapp (C++17, -g3 -O0) |
||||||
|
``` |
||||||
|
|
||||||
|
## Build and run standalone |
||||||
|
|
||||||
|
You don't need docker-compose or the devstation container — the remote works |
||||||
|
on its own. |
||||||
|
|
||||||
|
```bash |
||||||
|
# Build (run from repository root) |
||||||
|
docker build -t dap-debug-remote -f remote/Dockerfile . |
||||||
|
|
||||||
|
# Run |
||||||
|
docker run -d --name remote \ |
||||||
|
-p 13000:13000 \ |
||||||
|
--cap-add SYS_PTRACE \ |
||||||
|
--security-opt seccomp:unconfined \ |
||||||
|
dap-debug-remote |
||||||
|
``` |
||||||
|
|
||||||
|
`SYS_PTRACE` and `seccomp:unconfined` are required so codelldb can control the |
||||||
|
child process (set breakpoints, read memory, single-step). |
||||||
|
|
||||||
|
To use a different external port, set `DAP_PORT`: |
||||||
|
|
||||||
|
```bash |
||||||
|
docker run -d --name remote -p 8080:8080 -e DAP_PORT=8080 \ |
||||||
|
--cap-add SYS_PTRACE --security-opt seccomp:unconfined \ |
||||||
|
dap-debug-remote |
||||||
|
``` |
||||||
|
|
||||||
|
### Connecting to a non-local host |
||||||
|
|
||||||
|
`debugServer` and most editors connect to `localhost`. If the remote container |
||||||
|
runs on another machine, tunnel the port over SSH: |
||||||
|
|
||||||
|
```bash |
||||||
|
ssh -L 13000:localhost:13000 user@remote-host |
||||||
|
``` |
||||||
|
|
||||||
|
--- |
||||||
|
|
||||||
|
## Connect from VSCode |
||||||
|
|
||||||
|
### Prerequisites |
||||||
|
|
||||||
|
Install the **CodeLLDB** extension (`vadimcn.codelldb`) from the VSCode |
||||||
|
marketplace. This registers the `"lldb"` debug type, which VSCode needs even |
||||||
|
when connecting to an external DAP server. |
||||||
|
|
||||||
|
### Configure launch.json |
||||||
|
|
||||||
|
Open `common/project/` as your workspace folder in VSCode (so source paths |
||||||
|
match), then create `.vscode/launch.json`: |
||||||
|
|
||||||
|
```jsonc |
||||||
|
{ |
||||||
|
"version": "0.2.0", |
||||||
|
"configurations": [ |
||||||
|
{ |
||||||
|
"name": "Remote launch (codelldb :13000)", |
||||||
|
"type": "lldb", |
||||||
|
"request": "launch", |
||||||
|
"program": "/project/build/debugapp", |
||||||
|
"cwd": "/project", |
||||||
|
"stopOnEntry": false, |
||||||
|
"sourceMap": { |
||||||
|
"/project": "${workspaceFolder}" |
||||||
|
}, |
||||||
|
"debugServer": 13000 |
||||||
|
} |
||||||
|
] |
||||||
|
} |
||||||
|
``` |
||||||
|
|
||||||
|
| Field | Why | |
||||||
|
|---|---| |
||||||
|
| `type: "lldb"` | Tells VSCode to use the CodeLLDB extension's session handler | |
||||||
|
| `request: "launch"` | codelldb on the remote starts the program for you | |
||||||
|
| `program` / `cwd` | Paths **inside the container** (not local) | |
||||||
|
| `sourceMap` | Maps container paths (`/project`) to your local workspace folder so VSCode opens the right source files | |
||||||
|
| `debugServer: 13000` | **Key field** — tells VSCode to connect to an already-running DAP server on this port instead of launching codelldb locally | |
||||||
|
|
||||||
|
Set breakpoints in `src/calculator.cpp`, press **F5** (or Run ▸ Start |
||||||
|
Debugging), and select the configuration. The program runs in the container |
||||||
|
and stops at your breakpoints. |
||||||
|
|
||||||
|
### Without the CodeLLDB extension |
||||||
|
|
||||||
|
If you don't want to install CodeLLDB, use the **webfreak.debug** extension |
||||||
|
(`debug`) which provides a generic `"type": "cppdbg"` adapter. However, the |
||||||
|
CodeLLDB approach above is recommended because it speaks the same DAP dialect |
||||||
|
as the server. |
||||||
|
|
||||||
|
--- |
||||||
|
|
||||||
|
## Connect from Neovim (your own installation) |
||||||
|
|
||||||
|
Add this to your nvim-dap configuration: |
||||||
|
|
||||||
|
```lua |
||||||
|
local dap = require("dap") |
||||||
|
|
||||||
|
-- Connect to codelldb running on the remote container. |
||||||
|
dap.adapters.codelldb = { |
||||||
|
type = "server", |
||||||
|
host = "localhost", -- or the remote host IP |
||||||
|
port = 13000, |
||||||
|
} |
||||||
|
|
||||||
|
dap.configurations.cpp = { |
||||||
|
{ |
||||||
|
name = "Remote launch (codelldb :13000)", |
||||||
|
type = "codelldb", |
||||||
|
request = "launch", |
||||||
|
program = "/project/build/debugapp", |
||||||
|
cwd = "/project", |
||||||
|
stopOnEntry = false, |
||||||
|
}, |
||||||
|
} |
||||||
|
dap.configurations.c = dap.configurations.cpp |
||||||
|
``` |
||||||
|
|
||||||
|
Then `<leader>dc` (or `:DapContinue`) and select the configuration. |
||||||
|
|
||||||
|
> Using the included **devstation** container? The adapter configuration is |
||||||
|
> already baked in — see [devstation/README.md](../devstation/README.md). |
||||||
|
|
||||||
|
--- |
||||||
|
|
||||||
|
## Connect from Emacs (dap-mode) |
||||||
|
|
||||||
|
```elisp |
||||||
|
(require 'dap-codelldb) |
||||||
|
|
||||||
|
;; Tell dap-mode to connect to the remote DAP server instead of launching |
||||||
|
;; codelldb locally. |
||||||
|
(dap-register-debug-template |
||||||
|
"Remote launch (codelldb :13000)" |
||||||
|
(list :type "codelldb" |
||||||
|
:request "launch" |
||||||
|
:program "/project/build/debugapp" |
||||||
|
:cwd "/project" |
||||||
|
:stopOnEntry nil |
||||||
|
:dap-server-host "localhost" |
||||||
|
:dap-server-port 13000)) |
||||||
|
``` |
||||||
|
|
||||||
|
Run `M-x dap-debug` and select the template. See the |
||||||
|
[dap-mode wiki](https://github.com/emacs-lsp/dap-mode#codelldb) for details. |
||||||
|
|
||||||
|
--- |
||||||
|
|
||||||
|
## Connect from any DAP client |
||||||
|
|
||||||
|
The remote exposes a standard DAP server on port 13000. Any tool that can |
||||||
|
act as a DAP client can connect: |
||||||
|
|
||||||
|
- **CLI testing** — send a raw DAP `initialize` request to verify the server |
||||||
|
is alive: |
||||||
|
```bash |
||||||
|
echo '{"command":"initialize","arguments":{"adapterID":"test"},"type":"request","seq":1}' | nc localhost 13000 |
||||||
|
``` |
||||||
|
- **Custom tooling** — implement the |
||||||
|
[DAP client side](https://microsoft.github.io/debug-adapter-protocol/) of |
||||||
|
the protocol and connect to `localhost:13000`. |
||||||
|
|
||||||
|
--- |
||||||
|
|
||||||
|
## Environment variables |
||||||
|
|
||||||
|
| Variable | Default | Description | |
||||||
|
|---|---|---| |
||||||
|
| `DAP_PORT` | `13000` | External port that socat listens on (mapped to `0.0.0.0`) | |
||||||
|
|
||||||
|
codelldb's internal port (13001) is fixed and not configurable from outside. |
||||||
|
|
||||||
|
## Troubleshooting |
||||||
|
|
||||||
|
**"Connection refused"** |
||||||
|
Make sure the container is running (`docker ps`) and the port is mapped |
||||||
|
(`-p 13000:13000`). Check logs: `docker logs remote`. |
||||||
|
|
||||||
|
**Breakpoints don't hit** |
||||||
|
Ensure you are setting breakpoints in the source files and that the |
||||||
|
configuration uses `request: "launch"` (not `"attach"`). With `launch`, |
||||||
|
codelldb starts the program itself. |
||||||
|
|
||||||
|
**Source files not found in VSCode** |
||||||
|
The `sourceMap` in `launch.json` must map `/project` to your local workspace |
||||||
|
folder. If you opened the repo root instead of `common/project/`, adjust the |
||||||
|
mapping: `"sourceMap": { "/project": "${workspaceFolder}/common/project" }`. |
||||||
|
|
||||||
|
**Can't reconnect after ending a session** |
||||||
|
The entrypoint restarts codelldb automatically. Wait ~1 second after |
||||||
|
terminating a session before starting a new one. |
||||||
@ -0,0 +1,38 @@ |
|||||||
|
#!/bin/sh |
||||||
|
# |
||||||
|
# Remote container entrypoint. |
||||||
|
# |
||||||
|
# codelldb binds to 127.0.0.1 only (no --host flag), so socat forwards |
||||||
|
# connections from 0.0.0.0:13000 to codelldb on 127.0.0.1:13001. |
||||||
|
# |
||||||
|
# The developer station's nvim-dap connects to port 13000 and sends DAP |
||||||
|
# launch/attach requests. After each session ends, codelldb exits; this |
||||||
|
# loop restarts both processes so the developer can reconnect without |
||||||
|
# restarting the container. |
||||||
|
|
||||||
|
set -eu |
||||||
|
|
||||||
|
EXTERNAL_PORT="${DAP_PORT:-13000}" |
||||||
|
INTERNAL_PORT=13001 |
||||||
|
ADAPTER="/opt/codelldb/extension/adapter/codelldb" |
||||||
|
|
||||||
|
while true; do |
||||||
|
echo "[remote] codelldb DAP server on :${EXTERNAL_PORT} (forwarded to 127.0.0.1:${INTERNAL_PORT})..." |
||||||
|
|
||||||
|
# socat forwards external TCP to codelldb's localhost listener. |
||||||
|
socat TCP-LISTEN:"${EXTERNAL_PORT}",reuseaddr,fork \ |
||||||
|
TCP:127.0.0.1:"${INTERNAL_PORT}" & |
||||||
|
SOCAT_PID=$! |
||||||
|
|
||||||
|
# Give socat a moment to bind before codelldb starts accepting. |
||||||
|
sleep 0.2 |
||||||
|
|
||||||
|
# codelldb handles one --multi-session instance; exits when client disconnects. |
||||||
|
"$ADAPTER" --port "$INTERNAL_PORT" --multi-session |
||||||
|
|
||||||
|
kill "$SOCAT_PID" 2>/dev/null || true |
||||||
|
wait "$SOCAT_PID" 2>/dev/null || true |
||||||
|
|
||||||
|
echo "[remote] DAP session ended. Restarting in 1 second..." |
||||||
|
sleep 1 |
||||||
|
done |
||||||
Loading…
Reference in new issue