Skip to content

Latest commit

 

History

History
291 lines (244 loc) · 20.8 KB

20-projects.md

File metadata and controls

291 lines (244 loc) · 20.8 KB

Project kinds

mcppls does not ask you to describe your build. It finds it, and says what it found in the status bar. This is what "finding it" means for each kind of project, and what you get when it cannot.

The status bar's L1..L4 is the tier: how the project was described — L1 a build database (mcpp's emit build-database, or one of your own), L2 CMake's own database, L3 a bare compile_commands.json (including the one an mcpp too old to emit a build database leaves, and the ones xmake and meson write), L4 sources only (an untrusted workspace is always L4, whatever else is on disk). It is not the same number as the level mcppls check and cxxModules/status also carry, which is S1's own 1..4 for how completely a database's document is structured; a hand-written level-3 database and an mcpp build database are both L1, and a CMake project without FILE_SET CXX_MODULES is L2 even at level 1. The status bar and the editor plugins show L<tier> only, to keep the two apart. A model is never replaced by one of a worse tier: if the build tool later answers less completely than the model in hand, that model is kept and the status says it may be stale.

mcpp

mcppls asks mcpp for the build description directly:

mcpp emit build-database --format json

mcpp answers with a document that names every translation unit, its module role, its arguments and the standard library module it uses — without building anything and without writing into your project. That is the best case: the description comes from the tool that owns the build.

A few things are worth knowing:

  • How long it may take, and when it runs again. The build tool's hard deadline is 5 minutes the first time, then three times what its last run took, between 1 and 10 minutes; the project's cached model serves meanwhile, or its sources when there is none (mcppls.producerTimeout sets a fixed limit instead). Saving a source does not run it: a saved source whose module declaration and imports are unchanged only updates the index. When conditional compilation excludes an import, a successful description can still confirm that source structure, so later body-only saves do not repeatedly ask for the same graph. Preprocessing changes, failed descriptions and files changed while discovery runs retain conservative rechecks. The build tool is asked again when a build file, a non-source input or a source's module structure changes, and edits that keep coming are one run — the wait before it grows with how long the build tool takes, up to a minute. A save of a file you are still typing in (autosave while you type an import) waits until that file has been quiet for two seconds, at most thirty, so a typed import is one run, made once you pause.

  • The run is offline. A build description is a question about the project, not an errand, so the server asks it with MCPP_OFFLINE set. If the project's dependencies are not on the machine yet, mcpp says so, and nothing waits for you to decide anything:

    • the project is served from its sources at once (L4), and whatever mcpp could describe is used;
    • a notification in the corner offers Download and Continue (the build tool may reach the network, this once), Run in Terminal (where your proxy and credentials are), or Don't Ask Again. You can leave it unanswered forever; it is asked once per workspace and set of missing things;
    • the description is asked again, offline, after 30 s, 1 and 2 minutes and then every 5, and at once when mcpp.toml or mcpp.lock changes — so if you build in your own terminal instead, the project upgrades by itself and the question no longer applies.

    See 30-settings.md for mcppls.buildTool and mcppls.buildDiscovery.askBeforeDownload.

  • What a build rule generates. A rule package (mcpp:plugins' rules-qt, say) turns .ui, .qrc and .ts files into headers and sources when the project builds. mcpp describes the build without running those steps, so a form's ui_*.h does not exist yet in what it describes. mcppls leaves the rule's inputs out (they are not C++), reads the generated files from your project's own target/ when a build has written them, and otherwise says which are missing, with Build in Terminal; once a build writes them, the files that include them get their semantics without a restart.

  • build.mcpp and the libraries it imports. From mcpp 2026.10.11.1 the build database also describes the build programs: build.mcpp, the libraries of [build-dependencies] and mcpp's engine interface (import mcpp;), in sets of their own configuration (<package>:build, <package>:host, mcpp:core). build.mcpp is read as C++ with the arguments mcpp compiles it with, so navigation, hover, completion and diagnostics reach its libraries and the engine interface. A package in both [dependencies] and [build-dependencies] is read as the targets build it; the context <package>:build (mcppls: Select Context) reads the build programs as mcpp compiles them, and is where they are read when they use a toolchain of their own, as in a cross build. An import across the two — a target importing mcpp or a library of [build-dependencies], build.mcpp importing a module of the targets — is an error, as it is for mcpp. The engine interface under <MCPP_HOME>/engine/<version>/modules belongs to mcpp and is read-only: no formatting, code action or rename edits it, and VS Code opens it read-only. Renaming a symbol declared or defined there is also refused from project code, so it cannot leave a partial rename. Project and writable library symbols remain editable; if ownership cannot be resolved, the rename returns an error without edits. With an older mcpp, build.mcpp is not read, and a note in the file names the mcpp that describes it.

  • An older mcpp without emit build-database is not the end of it: if a newer mcpp is installed elsewhere on the machine (the xlings package store, mcpp's own registry store), mcppls asks that one instead, read-only and offline the same way, only to describe the project — the project still builds with the mcpp it pins. The status says "described by mcpp X (the project pins Y)" when this happens. Only when no installed mcpp can answer is the pinned one asked to configure instead (mcpp build --configure-only), which does write into the project; the status says so then, and updating mcpp is the fix.

  • A stale database — one whose entries name files a target/ a build once wrote and the project later deleted, or a compile_commands.json checked in from another machine — is used for what still exists; the status notes it is stale rather than failing outright. A module that build writes only when it runs (a dependency's own std, a code generator's output) is looked for where builds leave it — the project's own build directory, and mcpp's build-database cache, which survives a deleted target/ — before mcppls falls back to an empty stand-in for it.

Following an mcpp build target

By default (mcppls.buildDiscovery.mcppTarget = "auto"), mcppls follows an explicit, unique target from the project's root compile_commands.json, then asks mcpp to describe that target. For example, after mcpp build --target i686-windows-msvc publishes that database, analysis follows i686 without another editor setting. This follows the latest published compilation configuration, which can also come from a configure-only command or a failed compilation; it does not certify a successful build. Project ownership recognizes Windows 8.3 and long directory spellings as the same location; the compilation database's original paths and arguments are preserved. On Windows with MSVC, an mcpp project that uses import std needs mcpp 2026.10.5.2 or later: earlier releases pass the Windows SDK path with a trailing backslash that swallows the next argument, the standard library modules fail to build, and mcppls shows mcpp's own error as producer-failed. For a 32-bit target (i686-windows-msvc), import std is not available with Clang at all: Clang does not support coroutines on 32-bit x86 MSVC, so the MSVC STL's std module cannot be built, and mcppls reads such a project with the semantic kit and says so (std-fallback-kit).

To fix this workspace's analysis target, put this in .vscode/settings.json (or use the corresponding LSP initialization setting in another editor):

{
  "mcppls.buildDiscovery.mcppTarget": "i686-windows-msvc"
}

Use "project" to ignore the compilation database for target selection and always let mcpp apply its own defaults, including [build].target in mcpp.toml. CLI users can pass --mcpp-target. The setting only affects the mcpp provider and does not enable execution when buildTool is off. Untrusted workspaces keep their source-scan fallback; selecting a target never enables a producer or compiler there. In VS Code, the explicit setting applies to the whole workspace window; independent folder overrides in a multi-root window are not supported. Automatic selection still reads each root's own CDB independently. Changing the target reloads the model without restarting the language server.

Automatic selection uses explicit --target / -target arguments, without executing CDB commands. Missing, conflicting or unsupported target-changing inputs fall back to mcpp defaults with an explanation in status/report. No architecture is guessed from macros, directory names or file dates. An incomplete database being written keeps the previous selection during a bounded retry. Invalid explicit settings are reported instead of silently using the host platform.

The status shows the adopted platform. While a switch is pending or fails, the requested target is shown separately and the previous model can remain available. Target selection does not recover profile, features or the complete toolchain from CDB arguments. To use the database's complete commands, select only mcppls.buildDiscovery.providers = ["compile-commands"]; mcppls.database accepts an S1 build database rather than a compile_commands.json.

Cached mcpp models from older versions without target-selection identity are regenerated on the first session after upgrade. The initial project description can therefore take longer once.

CMake

If the build directory has a build_database.json (CMake 4.4+ with Ninja, FILE_SET CXX_MODULES), mcppls reads it. Otherwise it reads compile_commands.json and expands the @modmap files the generator wrote.

A build directory is looked for in build*/, out/build/*, cmake-build-*, and where the first configure preset of CMakePresets.json (or CMakeUserPresets.json) puts its binaryDir.

With no build directory at all and a trusted workspace, mcppls configures one of its own, under its cache directory — never in your project — following that preset's generator, toolchain file and cache variables, so it describes the build you would get. That configure is disconnected (-DFETCHCONTENT_FULLY_DISCONNECTED=ON), the first time too: a FetchContent dependency that is not on the machine stops it, and you get the same non-blocking offer as for mcpp above (Download and Continue configures once with the network, in that private directory).

xmake

xmake.lua makes an xmake project. mcppls asks xmake itself, with its own command, xmake project -k compile_commands, which compiles nothing — but it configures and scans modules, so mcppls points xmake's configuration, build and temporary directories at its cache (XMAKE_CONFIGDIR, --builddir, XMAKE_TMPDIR) and your project stays untouched: nothing is generated, changed or deleted in it, your own compile_commands.json included, and your own builds do not read anything it wrote (with GCC, xmake keeps module mapper files in its temporary directory under names that only the source's relative path decides; 0.0.16 wrote them there, and your next incremental build failed with failed to read compiled module, #69). That private state is a cache of mcppls's: it is made again from nothing whenever xmake itself, any xmake.lua or your xmake.conf changes, once more when xmake says it is out of date (dependfile ... not found, a scan cache that only a build updates; #68), and resetting the workspace cache removes it. It runs offline (--policies=package.fetch_only,network.mode:private): a package that is not installed stops it with the same offer as above. The first description takes a few seconds (about 6–8 s measured, most of it xmake detecting the toolchain); the project is served from its sources meanwhile. Module roles come from scanning, so an xmake project is L3.

The model follows what you do, with nothing to run by hand: a change to any xmake.lua describes the project again, and so does your own xmake f — mcppls reads what it left in .xmake/<plat>/<arch>/xmake.conf (read only, the newest one when there are several) and configures its private run the same way: platform, architecture, mode (-m debug gives -O0 -g, not the release flags), toolchain, SDK, runtimes, kind and the options your own xmake.lua declares. Which keys of that file are options is asked of xmake: mcppls runs xmake f --help once in the project (kept in its cache until xmake or xmake.lua changes) and passes on only the keys that list names. The rest are what xmake wrote for itself (proxy, dotnet, dotnet_sdkver, toolchain probes): xmake f refuses them, so they are skipped, and nothing is said about it. Only when xmake gives no usable help and then refuses an option does mcppls configure again with the standard ones only, and the status says which were left out.

When xmake needs packages it does not have (add_requires, and what they need to build, build tools such as libtool or meson included), the run stops, as it stays offline: the status says which packages, and offers to download them, to run xmake in your terminal, or to install them with your system package manager, which also works because xmake uses the system's package when it finds one. If the network is allowed (mcppls.buildTool = online, or you chose to download) and the install fails, the status says producer-install-failed instead, naming the packages with xmake's own error lines and the path of its install log, and does not offer the same download again.

A compile_commands.json of yours, at the root or in .vscode/ (where xmake's VS Code plugin writes one), is not read while mcppls can run xmake: it only follows your last xmake project, not your xmake.lua, so the two would take turns. It is read as it is when mcppls cannot run xmake — the workspace is not trusted, xmake is not on PATH, or mcppls.buildTool is off — and then it is watched, and a notice says so once when it is older than an xmake.lua (or your xmake.conf): run xmake project -k compile_commands to update it. To have mcppls read your own file on purpose, set mcppls.buildTool to off.

meson

meson.build makes a meson project. An existing build directory (builddir/, build/, or any directory with meson-private/) is read for its compile_commands.json. Otherwise mcppls runs meson setup into its cache with --wrap-mode=nodownload; a subproject that would have to be downloaded stops it with the same offer. L3, like xmake.

Turning discovery off

mcppls.buildDiscovery.mode = off makes mcppls detect no build system at all: nothing is read or run implicitly, and only a database you name with mcppls.database is used, else the sources are scanned (L4). mcppls.buildDiscovery.providers leaves out single build systems instead — for example, only ever read an existing CMake build directory and never run xmake. mcppls.buildTool = off is the narrower switch: build systems are still detected and their existing output read, only never run. See 30-settings.md.

compile_commands.json

Any compile_commands.json works, whoever wrote it. mcppls scans the sources named in it to recover module roles the database does not carry, and probes the compilers it names for their module facts.

No build system

Sources are scanned, module roles inferred from what they declare, and a compiler is looked for on the machine. This is the fallback, and it is the reason opening a directory of .cppm files gives something useful rather than nothing.

No compiler either

The bundled semantic kit takes over: libc++ built as modules, with a manifest that says where each module is. import std resolves, and so do the modules of your own code. Diagnostics come from a standard library that is not the one you build with, so they can differ — the status bar says the profile is a semantic kit rather than a build toolchain.

Which C++ standard, and C++26

The standard is the build's: whatever -std= (or /std:) a unit's command says, mcppls gives clangd; /std:c++latest is C++26. Three rules go further.

  • One standard per context for module units. A module's BMI can only be imported under the standard it was built with — import std in a C++26 file fails outright when std was built as C++23 ("C++26 was disabled in precompiled file"). So the units of one context that import, provide or belong to a module are read with the newest standard among them, and a plain unit keeps its own; the log says when some were raised, and the report's plan.languageStandard, plan.standardsSeen and plan.standardsRaised say which. The status profile names the standard.
  • A command that names no standard (xmake without set_languages, for one). Its module units -- anything that imports, provides or belongs to a module, std's own unit included -- are read as C++23 (gnu++23; c++23 for an MSVC target), the standard import std is made for, whatever the compiler's own default: Clang's gnu++17 has no modules at all. The log says it once and the report's plan.standardAssumed is true; a standard the build does name is followed instead. A plain unit is read with its build compiler's own default where that differs from Clang's (GCC 16: gnu++20; C from GCC 15: gnu23), as the build compiles it. Module units a build names a standard older than C++20 for are said once (module-standard-too-old): modules need C++20.
  • Sources nothing describes are read with the newest standard their compiler takes: C++26 with the semantic kit (clang 23, libc++ 23), GCC 14 and later, and Clang 17 and later (spelled c++2c before Clang 20); C++23, the oldest standard with import std, for older compilers.

What C++26 gives you is clangd 23.1's: pack indexing, = delete("reason"), placeholder _, static_assert messages, #embed, variadic friends and the rest of what clang 23 implements, and the C++26 library of the standard library you build with (the kit's is libc++ 23). Contracts (P2900) and reflection (P2996) are not in clang 23: code using them shows clang's errors even where GCC compiles it; VS Code still colors contract_assert, pre and post.

Standard library macros

std is built with only the standard library's own configuration macros — _ITERATOR_DEBUG_LEVEL, _DEBUG, _GLIBCXX_ASSERTIONS, _LIBCPP_HARDENING_MODE, _HAS_* and the like — not with every -D of your project. A project macro that happens to be a header guard of the standard library would otherwise change what std contains: GalTranslPP defines _RANGES_ for its units, which is MSVC STL's guard for <ranges>, and std built with it had no std::views ("no member named 'views' in namespace 'std'"). The macros left out are named in the log. Your own units keep all their macros.

An untrusted workspace

No build tool and no compiler is run at all — VS Code's workspace trust is respected before anything else. The semantic kit provides the semantics and the status says why.

What the status bar tells you

It says It means
A compiler and standard library The model came from your build; that toolchain's std is in use
A semantic kit No usable compiler was found, or the workspace is untrusted
"may be stale" The build tool could not answer this time; the last model that loaded is still in use
"needs a download" Planning offline stopped at something not on the machine; the project is served from its sources meanwhile, and there are actions to fix it (or build in your terminal: it upgrades by itself)
"files the build generates … do not exist yet" A rule's output (a Qt form's header, say) is not built yet; build once and the files that include it get their semantics
"implementation unit(s) cannot be read" An implementation unit does not build (a missing header, usually), so go-to-definition cannot reach the definitions in it

How to lay out a module project so it builds and completes fast — wrapper modules, each header in one place, import std without standard #includes — is in 25-module-projects.md.