Skip to content

Instantly share code, notes, and snippets.

@h1romas4
Last active December 22, 2025 02:59
Show Gist options
  • Select an option

  • Save h1romas4/7399dd3985bd4dae42e8c01ee343b233 to your computer and use it in GitHub Desktop.

Select an option

Save h1romas4/7399dd3985bd4dae42e8c01ee343b233 to your computer and use it in GitHub Desktop.
PlatformIO: Fix compile_commands for clangd (`pio_fix_compiledb.py` + `.clangd`)
CompileFlags:
Compiler: xtensa-esp32s3-elf-gcc
Add: [-DEXAMPLE_DEFINE1=1, -DEXAMPLE_DEFINE2]
Remove:
- -m*
- -fstrict-volatile-bitfields
- -fno-tree-switch-conversion
CompilationDatabase: .pio/build/esp32-s3-devkitc-1

PlatformIO: Fix compile_commands for clangd

This gist contains pio_fix_compiledb.py and a recommended .clangd to generate and post-process compile_commands.json from PlatformIO so clangd gets correct -I paths (library entries under lib/ inherit includes from the project's main source). Use as a pair:

$ pio --version
PlatformIO Core, version 6.1.18
$ pio run -t compiledb && pio run -t fix_compiledb

Notes:

  • Restricts include collection to one toolchain to avoid mixing multiple installed toolchains' include paths.
  • SPDX license: CC0-1.0
# SPDX-License-Identifier: CC0-1.0
# pyright: reportMissingImports=false, reportUndefinedVariable=false
# flake8: noqa
"""
Utility build hook to generate and fix a compile_commands.json suitable for
clangd. Prepares the compilation database and propagates -I flags to library
entries under `lib/`.
Additionally, in environments with multiple toolchains installed, this script restricts
include paths to a single specified toolchain to ensure deterministic include
resolution.
Usage (must be run as a pair):
pio run -t compiledb && pio run -t fix_compiledb
Run `compiledb` first, then `fix_compiledb`.
Version Info (pio --version)
PlatformIO Core, version 6.1.18
"""
import glob
import json
import os
import sys
import shlex
Import("env") # type: ignore
def pre_compiledb_actions(toolchain, framework_libs=None):
"""
Prepare compile_commands.json for clangd:
- restrict CPPPATH to the specified toolchain and Arduino framework libs
- set COMPILATIONDB_PATH to the build directory
framework_libs_subdirs: optional list of sub-path patterns under PROJECT_PACKAGES_DIR
e.g. [os.path.join("arduinoespressif32", "libraries")]
"""
print("start pre_compiledb_actions")
# turn off default toolchain includes
env.Replace(COMPILATIONDB_INCLUDE_TOOLCHAIN=False)
# restrict CPPPATH to xtensa-esp32s3-elf include paths to avoid mixing toolchains.
toolchain_paths = [
p for p in env.DumpIntegrationIncludes().get("toolchain", []) if toolchain in p
]
# add path(s) to Arduino framework libraries (support multiple framework dirs)
packages_dir = env.get("PROJECT_PACKAGES_DIR")
framework_libs = framework_libs or []
framework_libs_dirs = []
for sub in framework_libs:
# join packages_dir with the provided sub-path pattern and glob
pattern = os.path.join(packages_dir, sub)
framework_libs_dirs.extend(glob.glob(pattern))
src_paths = []
for libraries_dir in framework_libs_dirs:
src_paths.extend(glob.glob(os.path.join(libraries_dir, "*", "src")))
libs_path = list(set(src_paths))
# replace CPPPATH for compiledb only
env.Replace(CPPPATH=toolchain_paths + libs_path)
# create compile_commands.json
env.Replace(COMPILATIONDB_PATH=os.path.join("$BUILD_DIR", "compile_commands.json"))
print("completed pre_compiledb_actions")
def fix_compiledb_action(*args, **kwargs):
"""
Post-process compile_commands.json:
- find the entry for the given project-relative source (args[1])
- extract its -I include flags and append missing ones to entries under 'lib/'
- write the file back only if modifications were made
"""
print("start fix_compiledb_action")
compile_commands_dir = os.path.normpath(args[0])
src_main = os.path.normpath(args[1])
compile_commands_path = os.path.join(compile_commands_dir, "compile_commands.json")
if not os.path.isfile(compile_commands_path):
print(
f"fix_compiledb_action: compile_commands.json not found at {compile_commands_path}",
file=sys.stderr,
)
return
compile_commands = []
with open(compile_commands_path, "r", encoding="utf-8") as fp:
compile_commands = json.load(fp)
match = next(
(
entry
for entry in compile_commands
if os.path.normpath(entry.get("file") or "") == src_main
),
None,
)
if not match:
print(f"fix_compiledb_action: no match {src_main}", file=sys.stderr)
return
cmd = match.get("command") or " ".join(match.get("arguments", []))
tokens = shlex.split(cmd)
includes = [t for t in tokens if t.startswith("-I")]
# propagate -I flags to entries under 'lib/'
lib_count = 0
if includes:
for entry in compile_commands:
rel = os.path.normpath(entry.get("file")).replace(os.sep, "/")
if rel.startswith("lib/"):
original_cmd = entry.get("command") or ""
existing_tokens = shlex.split(original_cmd)
# only append tokens that are not already present to avoid duplication
to_append = [inc for inc in includes if inc not in existing_tokens]
if to_append:
entry["command"] = original_cmd + (" " + " ".join(to_append))
lib_count += 1
if lib_count:
with open(compile_commands_path, "w", encoding="utf-8") as fp:
json.dump(compile_commands, fp, indent=2)
print(
f"Appended includes to {lib_count} lib entries in {compile_commands_path}"
)
print("completed fix_compiledb_action")
# pio run -t compiledb
if "compiledb" in COMMAND_LINE_TARGETS:
# Running `pio run -t compiledb` alone can aggregate include paths from all
# installed toolchains; to avoid that we explicitly restrict to a single
# toolchain here and collect includes only for it.
pre_compiledb_actions(
# ls -laF ~/.platformio/packages | grep toolchain
# drwx------ 6 hiromasa hiromasa 4096 10月 13 2024 toolchain-esp32ulp/
# drwx------ 7 hiromasa hiromasa 4096 9月 16 2024 toolchain-riscv32-esp/
# drwx------ 7 hiromasa hiromasa 4096 9月 16 2024 toolchain-xtensa-esp32s3/
# specify exactly one toolchain identifier, e.g. "xtensa-esp32s3-elf"
"xtensa-esp32s3-elf",
# ls -laF ~/.platformio/packages | grep frame
# drwx------ 6 hiromasa hiromasa 4096 10月 14 2023 framework-arduino-gd32v/
# drwx------ 6 hiromasa hiromasa 4096 9月 16 2024 framework-arduinoespressif32/
# drwx------ 8 hiromasa hiromasa 4096 10月 13 2024 framework-espidf/
# framework library subdirs under PROJECT_PACKAGES_DIR (joined with packages_dir inside function)
[
os.path.join("framework-arduinoespressif32", "libraries"),
],
)
# pio run -t fix_compiledb
env.AddCustomTarget(
"fix_compiledb",
None,
fix_compiledb_action(
# build directory where compile_commands.json is written (COMPILATIONDB_PATH)
# .pio/build/esp32-s3-devkitc-1
os.path.join(env.get("PROJECT_BUILD_DIR"), env.get("PIOENV")),
# source file whose compile_commands entry is used to extract -I include flags
# src/main.cpp
os.path.join("src", "main.cpp"),
),
)
[env:esp32-s3-devkitc-1]
# ...snip...
# ...snip...
# ...snip...
extra_scripts = pre:pio_fix_compiledb.py
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment