From 109d6824086502b6e9a7312f1bd9e1778e52b698 Mon Sep 17 00:00:00 2001 From: Ichthyostega Date: Thu, 1 Oct 2026 23:22:36 +0200 Subject: [PATCH] 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`. --- .clangd | 10 ++++++ SConstruct | 4 ++- admin/scons/CompileDB.py | 59 ++++++++++++++++++++++++++++++++ admin/scons/Options.py | 2 ++ doc/technical/build/SCons.txt | 63 ++++++++++++++++++++++++++++++++++- 5 files changed, 136 insertions(+), 2 deletions(-) create mode 100644 .clangd create mode 100644 admin/scons/CompileDB.py diff --git a/.clangd b/.clangd new file mode 100644 index 000000000..b428993c9 --- /dev/null +++ b/.clangd @@ -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 + diff --git a/SConstruct b/SConstruct index 147b8b7cd..6d459a61d 100644 --- a/SConstruct +++ b/SConstruct @@ -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') diff --git a/admin/scons/CompileDB.py b/admin/scons/CompileDB.py new file mode 100644 index 000000000..5c41ed20b --- /dev/null +++ b/admin/scons/CompileDB.py @@ -0,0 +1,59 @@ +# coding: utf-8 +## +## CompileDB.py - SCons build: generate a compilation database for clangd +## + +# Copyright (C) +# 2026, Hermann Vosseler +# +# **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 diff --git a/admin/scons/Options.py b/admin/scons/Options.py index 386c48884..06a30ffe6 100644 --- a/admin/scons/Options.py +++ b/admin/scons/Options.py @@ -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 diff --git a/doc/technical/build/SCons.txt b/doc/technical/build/SCons.txt index 7f56280d6..53c039c12 100644 --- a/doc/technical/build/SCons.txt +++ b/doc/technical/build/SCons.txt @@ -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)