Debugging and IDE integration

Coverage tools (gcov/gcovr), debuggers (GDB), and editor tooling (the VSCode C/C++ extension, clangd) all depend on paths recorded at compile time. Two scikit-build-core defaults make those paths disappear:

  • The build directory is temporary. Without build-dir set, scikit-build-core builds in a temporary directory that is deleted after the build — taking coverage data, debug artifacts, and compile_commands.json with it.

  • Build isolation hides everything else. pip install ., python -m build, uv build, and uv sync copy your project and install the build dependencies into throwaway environments, so recorded source paths and include paths no longer exist after the build.

The shared fix is a persistent build directory with isolation disabled (after preinstalling your build dependencies), so everything stays in place:

pip install --no-build-isolation -Cbuild-dir=build -ve .

You can set the build directory in pyproject.toml instead of on the command line:

[tool.scikit-build]
build-dir = "build"

Coverage and debugging (gcov / gcovr / GDB)

gcov records the source path and compilation directory in the .gcno (compile-time) and .gcda (run-time) files, and DWARF debug info records the source path in the compiled extension. If those paths no longer exist when you run the tool, you get errors such as

$ gcovr --xml coverage.xml -r .
(ERROR) Trouble processing '.../CMakeFiles/foo.dir/_foo.c.gcda' with working directory '/home'.

or GDB that cannot show source lines even with debug flags enabled.

With the persistent-tree install above, run gcovr against the persistent tree, pointing the root at your source:

gcovr -r . build

If you would rather not depend on the paths staying put, make the recorded paths relocatable with compiler flags — -fprofile-abs-path for gcov and -ffile-prefix-map=<build>=<src> (or -fdebug-prefix-map=... for debug info only) — for example via cmake.define or CFLAGS.

IDE IntelliSense (compile_commands.json)

Editor tooling resolves headers like pybind11/pybind11.h from your include paths. With build isolation, binding libraries such as pybind11 and nanobind live in a throwaway overlay — CMake reports a path such as .../Temp/pip-build-env-xxxx/overlay/Lib/site-packages/pybind11/include, which no longer exists when your editor looks for it.

Extend the shared fix by having CMake export a compile database:

pip install scikit-build-core pybind11
pip install --no-build-isolation --check-build-dependencies -ve . \
  -Cbuild-dir=build \
  -Ccmake.define.CMAKE_EXPORT_COMPILE_COMMANDS=1
uv pip install scikit-build-core pybind11
uv pip install --no-build-isolation -ve . \
  -Cbuild-dir=build \
  -Ccmake.define.CMAKE_EXPORT_COMPILE_COMMANDS=1

In a uv-managed project, disable isolation for your package instead, so uv sync reuses the environment’s build dependencies rather than a discarded overlay:

[tool.uv]
no-build-isolation-package = ["mypackage"]

This writes build/compile_commands.json with real, persistent paths. Then point your editor at the file. For clangd, add --compile-commands-dir=build (or a .clangd with a CompileFlags.CompilationDatabase: build entry); for the VSCode C/C++ extension, set "C_Cpp.default.compileCommands": "${workspaceFolder}/build/compile_commands.json". See editable installs for the related --no-build-isolation recommendations.

Debug builds on Windows

A cmake.build-type=Debug extension links against the debug CPython (pythonXY_d.dll, _d.pyd suffix), so it only loads under a debug interpreter (python_d.exe) — importing it under a normal python.exe crashes with 0x80000003. This is CPython behavior, not scikit-build-core’s: run the build itself under python_d.exe so the _d suffix and import library line up (and the t ABI flag is added for free-threaded debug builds, e.g. pythonXYt_d.dll).

To keep debug info while still loading under a normal Python, don’t do a debug build — instead undefine _DEBUG around the CPython headers so they don’t auto-link pythonXY_d.lib, and add debug flags yourself:

#ifdef _DEBUG
#  define SKB_RESTORE_DEBUG
#  undef _DEBUG
#endif
#include <Python.h>
#ifdef SKB_RESTORE_DEBUG
#  define _DEBUG
#endif