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)