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