Buildsystem: generate a compilation database for clangd (closes: #1436)

Knowledge-Technology(Claude Opus 5) used
 - to assess the situation and present implementation avenues
 - for discussion while shaping the solution
 - for generating code and validating results

Clang based tooling is used frequently for code analysis and navigation.
This requires however that the exact flags and invocation is captured for
each possible translation unit in the code base. That knowledge exists solely
in the build system, and the established way to hand it over is the
LLVM "JSON Compilation Database".

SCons has provided a `CompilationDatabase` builder since 4.0, and it fits
our setup well: it installs an emitter on the object builders and captures
each command line while the dependency graph is constructed. Hence nothing
is compiled -- generating takes about two seconds -- and the result always
describes the complete build graph as declared, rather than the target
actually requested on this invocation.

This feature is provided as ''opt-in'', either through the sticky option
`COMPILEDB=yes` or by direct invocation with `scons compiledb`

The `compile_commands.json` is generated into the build output `target/' dir
For configuration, a `.clangd` in project root instructs the language server
where to find this file.

__Note__ the ordering constraint recorded in `CompileDB.py`:
the emitter must be installed only after `Platform.configure()` has run,
or it also captures the `conftest_*` compilations of the configure checks.
That is why this is a plain helper invoked from the `SConstruct` -- similar to
Setup, Options, Platform -- and not a tool loaded from the `LumieraEnvironment`.
This commit is contained in:
Fischlurch 2026-10-01 23:22:36 +02:00
parent 595bb538be
commit 109d682408
5 changed files with 136 additions and 2 deletions

10
.clangd Normal file
View file

@ -0,0 +1,10 @@
# Configuration for the `clangd` language server -- https://clangd.llvm.org/config
#
# The compilation database is generated by the SCons build system into the target tree:
# scons COMPILEDB=yes (sticky: recorded in ./optcache)
# scons compiledb (one-off, compiles nothing)
# clangd does not search 'target/' on its own, hence this pointer.
CompileFlags:
CompilationDatabase: target

View file

@ -29,6 +29,7 @@ sys.path.append('./admin/scons')
import Setup
import Options
import Platform
import CompileDB
from Buildhelper import *
from LumieraEnvironment import *
@ -39,6 +40,7 @@ from LumieraEnvironment import *
env = Setup.defineBuildEnvironment() # dirs & compiler flags
env = Platform.configure(env) # library dependencies
compileDB = CompileDB.setup(env) # code intelligence (opt-in: COMPILEDB=yes)
@ -61,7 +63,7 @@ env.Clean ('build', [ 'scache.conf', '.sconf_temp', '.sconsign.dblite', 'config.
#Import('lumiera plugins tools gui testsuite doxydoc')
Import('lumiera plugins tools gui testsuite')
build = env.Alias('build', lumiera + plugins + tools + gui)
build = env.Alias('build', lumiera + plugins + tools + gui + compileDB)
# this defines the SCons **default target**
env.Default('build')

59
admin/scons/CompileDB.py Normal file
View file

@ -0,0 +1,59 @@
# coding: utf-8
##
## CompileDB.py - SCons build: generate a compilation database for clangd
##
# Copyright (C)
# 2026, Hermann Vosseler <Ichthyostega@web.de>
#
# **Lumiera** is free software; you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by the
# Free Software Foundation; either version 2 of the License, or (at your
# option) any later version. See the file COPYING for further details.
#####################################################################
from SCons.Script import COMMAND_LINE_TARGETS
def setup(env):
""" Emit a *compilation database*, describing every compiler invocation of the
complete build. This JSON file is the interface used by `clangd` and the other
tools built on the Clang front-end, which need to reproduce the exact flags
of a translation unit in order to parse it the way the build does.
The actual work is performed by the `CompilationDatabase` builder, which SCons
provides since 4.0: it installs an additional *emitter* on the object builders,
capturing each command line while the dependency graph is constructed. Thus no
compilation is involved -- `scons compiledb` takes about two seconds -- and the
database always covers the *complete declared build*, not just the requested
target, since the SConscript definitions are processed into a dependency tree
on every invocation anyway.
@return a dependency node, to be added to the `build` alias, or an empty list
when the feature is switched off (in which case nothing at all is
installed into the build environment).
@note **must be invoked after** `Platform.configure(env)`. The emitter would
otherwise also capture the `conftest_*` compilations performed by the
configure checks, which then show up as bogus entries below '.sconf_temp'.
This ordering constraint is why this module is a plain helper invoked
from the 'SConstruct' -- like `Setup`, `Options` and `Platform` -- and
not a SCons tool loaded from the `LumieraEnvironment` constructor.
@remark the database is written into the (relocatable) target tree, while the
'.clangd' configuration in the project root directs the language server
there; consumers do not search 'target/' on their own.
"""
if not isActivated(env): return []
env.Tool('compilation_db') # the builder provided by SCons
env['COMPILATIONDB_USE_ABSPATH'] = True # make recorded content unambiguous
compileDB = env.CompilationDatabase('$TARGDIR/compile_commands.json')
env.Alias('compiledb', compileDB)
return compileDB
def isActivated(env):
""" generation is opt-in through the (sticky) build option,
but can also be requested for a single invocation by naming the target.
"""
return env['COMPILEDB'] or 'compiledb' in COMMAND_LINE_TARGETS

View file

@ -34,6 +34,7 @@ def defineCmdlineVariables(buildVars):
,BoolVariable('OPTIMIZE', 'Build with strong optimisation (-O3)', False)
,BoolVariable('VALGRIND', 'Run Testsuite under valgrind control', True)
,BoolVariable('VERBOSE', 'Print full build commands', False)
,BoolVariable('COMPILEDB','Generate target/compile_commands.json for clangd', False)
,('TESTSUITES', 'Run only test suites matching the given pattern', '')
,('TESTMODE', 'test suite error mode for test.sh', '')
# ,BoolVariable('OPENGL', 'Include support for OpenGL preview rendering', False)
@ -58,6 +59,7 @@ Special Targets:
testcode: additionally compile the Testsuite
check : build and run the Testsuite
doc : generate documentation (Doxygen)
compiledb: just (re)generate target/compile_commands.json for clangd
all : build and testcode and doc
install : install created artifacts at PREFIX

View file

@ -238,6 +238,7 @@ Targets
- *check* builds test code and runs our test-suites
- *research* builds just the research tree
- *doc* builds documentation (currently just Doxygen)
- *compiledb* (re)generates the xref:CompilationDB[compilation database] -- compiles nothing
- *all* builds the Application, test-suites and documentation
- *install* builds and installs the Lumiera Application
@ -290,7 +291,8 @@ Neither was ever used in practice, and both are considered to be of marginal rel
Beyond being unused, both carried a concrete liability: each rewrote the `$CXX` construction
variable into `wrapper g++` -- a mutation that silently changes what any tool reading the
build commands will conclude about the compiler in use.
build commands will conclude about the compiler in use, the
xref:CompilationDB[compilation database] among them.
--
Configuration options
@ -380,3 +382,62 @@ to see examples of that technique -- which we also use to define that
global compound target variables like `core`, `vessel_lib`, `core_lib`,
`vault_lib`, and `support_lib`. These in turn are essential for
building the layered dependency hierarchy in our code.
[[CompilationDB]]
Compilation database
^^^^^^^^^^^^^^^^^^^^
The tooling built on the Clang front-end -- the https://clangd.llvm.org/[clangd] language
server, `clang-tidy`, `clang-format`'s include ordering, and various code browsers -- must
be able to parse a translation unit *exactly the way the build parses it*: same include
paths, same preprocessor definitions, same language standard. Since the build system is the
only place where that knowledge exists, the tools expect it to be handed over, in the form
of a JSON file listing one entry per compiler invocation. The format is specified by LLVM as
the https://clang.llvm.org/docs/JSONCompilationDatabase.html[JSON Compilation Database];
clangd's own https://clangd.llvm.org/design/compile-commands[notes on compile commands]
explain how a language server consumes it, and which heuristic it applies for header files,
which never appear in the database themselves.
Generation is *opt-in*, through the sticky option `COMPILEDB=yes`, or for a single
invocation by naming the `compiledb` target. Both write 'target/compile_commands.json'.
When the option is set, the database is a dependency of the `build` alias and is thus
refreshed by every ordinary build.
.How it works
SCons has provided a `CompilationDatabase` builder since version 4.0. Rather than
scraping build output, it installs an additional *emitter* on the object builders: whenever
an object node is created while the SConscript definitions are read, the emitter captures
the unexpanded command line together with the environment in force for that node. This
setup has several consequences:
Nothing is compiled::
Generating the database takes about two seconds, because it only requires
the dependency graph to be constructed, while no build commands will be evaluated.
All targets are captured::
The generated database always describes the complete build, not the requested target.
A plain `scons compiledb` covers 'src/', 'tests/', 'research/' and 'src/tool/' alike,
including the per-subtree flag differences -- the GTK include paths appear on the
'src/stage/' entries and nowhere else, `-Itests` is given only below 'tests/'.
Note that the captured command lines are *not* affected by the `VERBOSE` option: the
emitter reads the underlying `$CXXCOM` construction variable directly, not the abbreviated
progress messages configured in 'Setup.py'.
.How the feature is attached
The integration lives in 'admin/scons/CompileDB.py' and is invoked from the 'SConstruct'
through `CompileDB.setup(env)`, which returns the generated node so that the `build` alias
stays defined in one single, prominent place. Unlike the other custom tools, this one is
deliberately *not* loaded from the `LumieraEnvironment`: the emitter must be installed only
*after* `Platform.configure(env)` has run, since it would otherwise also capture the `conftest_*`
compilations of the configure checks and litter the database with entries below '.sconf_temp'.
.Consuming the database
clangd searches for 'compile_commands.json' beside the source and in a 'build/' subdirectory,
but not in our 'target/'. The '.clangd' configuration file in the project root supplies that
pointer, and is the place for any further language-server settings; its syntax is documented
under https://clangd.llvm.org/config[clangd configuration]. Note that the compilation
database describes what *GCC* is invoked with, which might not in every case be what Clang accepts;
Yet the database is still correct and useful in those spots; the parse simply degrades locally.
TIP: Lumiera embraces the policy to be compilable both with GCC and Clang
(but Clang support has fallen back at times, temporarily)