From 4c884095316ff432eaa21d07e5954a7f4ce98cd4 Mon Sep 17 00:00:00 2001 From: chodak166 Date: Sun, 12 Jul 2026 20:15:34 +0200 Subject: [PATCH] Initial commit --- README.md | 102 +++++++++ common/README.md | 26 +++ common/project/CMakeLists.txt | 20 ++ common/project/src/calculator.cpp | 88 ++++++++ common/project/src/calculator.h | 34 +++ common/project/src/main.cpp | 19 ++ devstation/Dockerfile | 92 +++++++++ devstation/README.md | 87 ++++++++ devstation/nvim-config/lua/plugins/dap.lua | 69 +++++++ devstation/nvim-config/lua/plugins/lsp.lua | 20 ++ devstation/nvim-config/lua/plugins/theme.lua | 9 + devstation/scripts/precompile-treesitter.lua | 32 +++ docker-compose.yml | 63 ++++++ remote/Dockerfile | 54 +++++ remote/README.md | 207 +++++++++++++++++++ remote/entrypoint.sh | 38 ++++ 16 files changed, 960 insertions(+) create mode 100644 README.md create mode 100644 common/README.md create mode 100644 common/project/CMakeLists.txt create mode 100644 common/project/src/calculator.cpp create mode 100644 common/project/src/calculator.h create mode 100644 common/project/src/main.cpp create mode 100644 devstation/Dockerfile create mode 100644 devstation/README.md create mode 100644 devstation/nvim-config/lua/plugins/dap.lua create mode 100644 devstation/nvim-config/lua/plugins/lsp.lua create mode 100644 devstation/nvim-config/lua/plugins/theme.lua create mode 100644 devstation/scripts/precompile-treesitter.lua create mode 100644 docker-compose.yml create mode 100644 remote/Dockerfile create mode 100644 remote/README.md create mode 100755 remote/entrypoint.sh diff --git a/README.md b/README.md new file mode 100644 index 0000000..210c011 --- /dev/null +++ b/README.md @@ -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 `db` on a line to set +a breakpoint, then `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. diff --git a/common/README.md b/common/README.md new file mode 100644 index 0000000..6f4b2af --- /dev/null +++ b/common/README.md @@ -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 | diff --git a/common/project/CMakeLists.txt b/common/project/CMakeLists.txt new file mode 100644 index 0000000..fe62fbf --- /dev/null +++ b/common/project/CMakeLists.txt @@ -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) diff --git a/common/project/src/calculator.cpp b/common/project/src/calculator.cpp new file mode 100644 index 0000000..aadac52 --- /dev/null +++ b/common/project/src/calculator.cpp @@ -0,0 +1,88 @@ +#include "calculator.h" + +#include +#include +#include + +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(a) / static_cast(b); + m_history.push_back(static_cast(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(); +} diff --git a/common/project/src/calculator.h b/common/project/src/calculator.h new file mode 100644 index 0000000..1f61876 --- /dev/null +++ b/common/project/src/calculator.h @@ -0,0 +1,34 @@ +#pragma once + +#include +#include + +/// 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 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); diff --git a/common/project/src/main.cpp b/common/project/src/main.cpp new file mode 100644 index 0000000..98411bc --- /dev/null +++ b/common/project/src/main.cpp @@ -0,0 +1,19 @@ +#include "calculator.h" + +#include + +int main() { + std::cout << "=== DAP Debugging Example ===" << std::endl; + std::cout << "Set breakpoints in calculator.cpp, then press 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; +} diff --git a/devstation/Dockerfile b/devstation/Dockerfile new file mode 100644 index 0000000..0d0bce0 --- /dev/null +++ b/devstation/Dockerfile @@ -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"] diff --git a/devstation/README.md b/devstation/README.md new file mode 100644 index 0000000..ff46072 --- /dev/null +++ b/devstation/README.md @@ -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: `db` +3. Start debugging: `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 `d` prefix: + +| Key | Action | +|---|---| +| `db` | Toggle breakpoint | +| `dB` | Conditional breakpoint | +| `dc` | Continue / start debug session | +| `dC` | Run to cursor | +| `di` | Step into | +| `dO` | Step over | +| `do` | Step out | +| `dp` | Pause | +| `dt` | Terminate session | +| `dr` | Toggle REPL | +| `du` | Toggle DAP UI | +| `de` | Evaluate expression (normal or visual mode) | +| `dw` | Hover (inspect variable under cursor) | +| `dj` / `dk` | Navigate call stack (down / up) | +| `dl` | Run last session | +| `dg` | Go to line (skip execution) | +| `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 `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. diff --git a/devstation/nvim-config/lua/plugins/dap.lua b/devstation/nvim-config/lua/plugins/dap.lua new file mode 100644 index 0000000..1eb62b8 --- /dev/null +++ b/devstation/nvim-config/lua/plugins/dap.lua @@ -0,0 +1,69 @@ +-- DAP plugin stack: core client, UI, inline virtual text. +-- Provides 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 = { + { "d", "", desc = "+debug", mode = { "n", "v" } }, + { "db", function() require("dap").toggle_breakpoint() end, desc = "Toggle Breakpoint" }, + { "dB", function() require("dap").set_breakpoint(vim.fn.input("Breakpoint condition: ")) end, desc = "Conditional Breakpoint" }, + { "dc", function() require("dap").continue() end, desc = "Continue / Start" }, + { "dC", function() require("dap").run_to_cursor() end, desc = "Run to Cursor" }, + { "dg", function() require("dap").goto_() end, desc = "Go to Line (skip execution)" }, + { "di", function() require("dap").step_into() end, desc = "Step Into" }, + { "dj", function() require("dap").down() end, desc = "Frame Down" }, + { "dk", function() require("dap").up() end, desc = "Frame Up" }, + { "dl", function() require("dap").run_last() end, desc = "Run Last" }, + { "do", function() require("dap").step_out() end, desc = "Step Out" }, + { "dO", function() require("dap").step_over() end, desc = "Step Over" }, + { "dp", function() require("dap").pause() end, desc = "Pause" }, + { "dr", function() require("dap").repl.toggle() end, desc = "Toggle REPL" }, + { "ds", function() require("dap").session() end, desc = "Session Info" }, + { "dt", function() require("dap").terminate() end, desc = "Terminate" }, + { "dw", function() require("dap.ui.widgets").hover() end, desc = "Hover Widget" }, + { "du", function() require("dapui").toggle() end, desc = "Toggle DAP UI" }, + { "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, + }, +} diff --git a/devstation/nvim-config/lua/plugins/lsp.lua b/devstation/nvim-config/lua/plugins/lsp.lua new file mode 100644 index 0000000..5bc7c12 --- /dev/null +++ b/devstation/nvim-config/lua/plugins/lsp.lua @@ -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 }, + }, + }, + }, +} diff --git a/devstation/nvim-config/lua/plugins/theme.lua b/devstation/nvim-config/lua/plugins/theme.lua new file mode 100644 index 0000000..3309c48 --- /dev/null +++ b/devstation/nvim-config/lua/plugins/theme.lua @@ -0,0 +1,9 @@ +return { + { + "catppuccin/nvim", + name = "catppuccin", + priority = 1000, + opts = { flavour = "mocha" }, + }, + { "LazyVim/LazyVim", opts = { colorscheme = "catppuccin" } }, +} diff --git a/devstation/scripts/precompile-treesitter.lua b/devstation/scripts/precompile-treesitter.lua new file mode 100644 index 0000000..c3df5e0 --- /dev/null +++ b/devstation/scripts/precompile-treesitter.lua @@ -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") diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..b7e8fc3 --- /dev/null +++ b/docker-compose.yml @@ -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 db, then press 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: diff --git a/remote/Dockerfile b/remote/Dockerfile new file mode 100644 index 0000000..903f93a --- /dev/null +++ b/remote/Dockerfile @@ -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"] diff --git a/remote/README.md b/remote/README.md new file mode 100644 index 0000000..9da0fda --- /dev/null +++ b/remote/README.md @@ -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 `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. diff --git a/remote/entrypoint.sh b/remote/entrypoint.sh new file mode 100755 index 0000000..b8b7186 --- /dev/null +++ b/remote/entrypoint.sh @@ -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