Repository navigation
feat(plugins): native plugins that load only into the build they target - #1374
Conversation
`MRDOCS_DECL` expanded to nothing in a static build, so it marked nothing and its misuse went unnoticed: it was applied to some classes and to some of their members, which MSVC rejects. `MRDOCS_TOOL` now comes first, so a plugin can call the symbols the headers declare, while a static consumer still sees plain declarations. Marking the API also exposed declarations that had no marker at all, such as the symbol kinds' `operator<=>` and several `Corpus` members, which a plugin reaches from inline and template code. We mark them now. The dom and handlebars libraries check build defines of their own in the same place, `MRDOCS_DOM_BUILDING` and `MRDOCS_HANDLEBARS_BUILDING`, which only their own targets set, so that their headers do not depend on the tool's. We also declare `assert_failed`, with `MRDOCS_DECL`, in every build mode: a plugin built without `NDEBUG` calls it from the inline code of the public headers, whatever mode MrDocs was built in.
MrDocs loads the shared libraries in the plugins subdirectory of each addon root as it starts up, and lets each one install a generator. A plugin resolves the MrDocs symbols it calls against the tool, so writing an output format takes the MrDocs headers and nothing else: no LLVM, no Clang, no separate library to ship. The tool links mrdocs-core privately now, so that a plugin linking the tool does not link the core as well. Note that a statically linked MrDocs, such as the Linux release, cannot load plugins. `MRDOCS_BUILD_PLUGIN_MODULES` is off by default there, and in a shared build, and a MrDocs built without it reports a plugin it finds instead of loading it. Closes cppalliance#58.
A plugin could install a generator, so it could add an output format but not change what any format sees. We now let a plugin install corpus transforms too, closing the gap with extension scripts. `loadPlugins` returns them in a `PluginRegistry`, which the tool applies before the script transforms, as it applies an `ExtensionRegistry`.
…le command MrDocs finds the default include directories by running the compiler of each compile command with `-v -E`. It ran that compiler without the command's `-isysroot` or `--sysroot`, so Xcode's toolchain `clang++` listed no SDK headers and `<cstdint>` was missing. It also treated a bare name such as `clang++` as a file path, found nothing, and fell back to guessing a compiler. The probe now passes the command's sysroot and looks a bare compiler name up on PATH, the way the driver runs it. It falls back to `$CXX`, `$CC` and common names only when the command's compiler cannot be found.
The guards that keep the rules of this branch from breaking later, and the sweeps that make the tree pass them: - lint, format and the release checks each live in their own directory under `utils/`, and every guard judges only what a branch changes - guards write SARIF, so CI and the review tools read the same findings - a layout check, a README length cap, section titles that name their concept, and unit tests that mirror the header they test - `.clang-format` describes the code as written - public type and function names in PascalCase, function names at column 0 after the qualifier line, and `Platform.hpp` first in the headers that need it, checked by the self-doc run - copyright lines where this branch changed existing files
- plugin support and the plugin tests are separate CMake options - `just` has a recipe for every test suite - the IDE run configs take program paths from CMake's file API, and refreshing them keeps the saved bootstrap arguments - each CMake helper is a file of its own, and a runtime DLL already next to its consumer is not linked again - the `find_tool` tests ignore the tool variables of the environment
…oever reports The global report functions become a `report::Reporter` that the session owns. The corpus, the extensions and the generators receive it by reference instead of reaching for global state, so two sessions in one process no longer share a log level or an error count. Breaking for library users: `session.log()` returns `report::Reporter&`, `Corpus::build` takes the reporter by reference, and scripts report through `ctx.report` instead of the global `mrdocs.report`.
… and plugins Scripts and native plugins now go through the same per-run pipeline: - one `extensions/` layout for scripts and plugins, and an extension is named by its file stem - the registries of transforms and generators belong to the context of the run - a script's `ctx` is the described C++ context, built next to its caller, with one builder per context type - a generator receives its options block the way a transform does - the extension contexts and callable wrappers live in headers of their own
This finishes the plugin support Gennaro started. A plugin is a shared library that registers transforms and generators with `registerTransform` and `registerGenerator`, each taking an id and a callable. A plugin sees MrDocs's C++ types by their layout, so it must match the exact build: - the loader (our own `dlopen` and `LoadLibraryEx`, never unloaded) refuses a plugin whose build id differs. The build id is the commit hash, with `.modified` for an edited tree, and comes from `git-archival.txt` in a source archive. - a plugin built against another standard library fails at its first compile, and the loader checks the tag again. - the tool exports only the MrDocs API. Each public class carries the marker its members need: `MRDOCS_DECL` when a member is compiled into MrDocs, `MRDOCS_VISIBLE` for type information only, and the self-doc run checks every case. - dom and handlebars symbols stay visible in a hidden core, and shared builds load plugins with the same recipe. Plugins link `mrdocs::plugin-api`, and `mrdocs_add_plugin` ships with the CMake package. The integration tests cover the refusals, symbol isolation, dependencies, reloading and a consumer built with another compiler. The user docs explain what must match and how to build a plugin.
- one directory per example with every variant and its tests, and the compiled plugin examples under `examples/extensions` - a load-and-run example for the library page - the plugin and CMake package suites under `tests/integration`, the plugin cases grouped into exports, refused, dependencies and reload, and the consumer test split into executable, plugin and library parts - golden migration fixtures named after the migration they show - library unit tests mirror their headers, and the tests share their setup instead of copying it
…hecks - the Linux release builds in ubuntu:20.04 for a glibc 2.31 floor and bundles the C++ runtime - the macOS release and its LLVM target macOS 14, and the release check fails when the deployment target is not set - shared rows for every compiler family - checkouts at depth 1, and push builds fetch the nearest calendar tag through a composite action - the pull request template is gone, with the danger check that read it
- a codebase layout page with the allowed top-level entries and a check, and one page per top-level directory instead of READMEs - the invariants in subpages, each naming the guard that checks it, with a page-length guard - a development setup page, a coding standards page and a contributor overview - copyable snippets come from built examples and tests, and links target explicit section anchors - extension names in the pipeline, template and integration pages, and the breaking-changes example shows sessions
Codex Review Summary: ❌ FailedThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
|
| Scope | Lines Δ% | Lines Δ | Lines + | Lines - | Files Δ | Files + | Files ~ | Files ↔ | Files - |
|---|---|---|---|---|---|---|---|---|---|
| 🛠️ Source | 24% | 10775 | 7640 | 3135 | 320 | 33 | 268 | 5 | 14 |
| 🧰 Tooling | 19% | 8365 | 7600 | 765 | 61 | 47 | 10 | 1 | 3 |
| 📦 Other | 15% | 6625 | 3884 | 2741 | 76 | 55 | 11 | 8 | 2 |
| 🧪 Unit Tests | 14% | 6071 | 4850 | 1221 | 32 | 16 | 11 | 3 | 2 |
| 📄 Docs | 12% | 5472 | 4463 | 1009 | 68 | 25 | 39 | 1 | 3 |
| 📚 Examples | 6% | 2453 | 2248 | 205 | 90 | 57 | 19 | 13 | 1 |
| ⚙️ CI | 3% | 1325 | 971 | 354 | 17 | 5 | 11 | - | 1 |
| 🥇 Golden Tests | 2% | 1070 | 821 | 249 | 65 | 48 | 6 | 2 | 9 |
| 🔧 Toolchain | 2% | 1003 | 670 | 333 | 13 | 1 | 12 | - | - |
| 🔧 Toolchain Tests | 2% | 772 | 684 | 88 | 8 | 3 | 5 | - | - |
| 🏗️ Build | <1% | 58 | 41 | 17 | 2 | - | 2 | - | - |
| Total | 100% | 43989 | 33872 | 10117 | 752 | 290 | 394 | 33 | 35 |
Legend: Files + (added), Files ~ (modified), Files ↔ (renamed), Files - (removed)
🔝 Top Files
- Doxyfile.url (Other): 2565 lines Δ (+0 / -2565)
- libs/dom/tests/unit/Dom/Value.cpp (Unit Tests): 956 lines Δ (+13 / -943)
- tests/unit/Extensions/AddonDiscovery.cpp (Unit Tests): 952 lines Δ (+952 / -0)
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## develop #1374 +/- ##
===========================================
+ Coverage 83.21% 83.44% +0.23%
===========================================
Files 35 36 +1
Lines 3670 3861 +191
Branches 845 891 +46
===========================================
+ Hits 3054 3222 +168
- Misses 410 417 +7
- Partials 206 222 +16
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
|
An automated preview of the documentation is available at https://1374.mrdocs.prtest2.cppalliance.org/index.html If more commits are pushed to the pull request, the docs will rebuild at the same URL. 2026-11-10 20:20:07 UTC |
2d29928
This builds on Gennaro's plugin work #1256. His pull request added native plugins, but it had problems that would have taken many review rounds, so I fixed them on top of his commits instead. His four commits come first, unchanged, and the ten after them are mine.
Plugins turned out to have a lot of moving parts because of how different operating systems load them into the current process. A plugin sees our C++ types directly by their layout and visibility rules between libraries, dependencies, and plugins that interact. To make things harder, GLIBC (and its equivalents in other OSs) are among these libraries, which means significant CI changes since plugins only work on a release built on an OS as old as the one the plugin is built on. Otherwise, we wouldn’t support plugins on Linux or macOS at all. On top of that, plugins only work when they match very specific build properties: the same commit/ABI, a compatible standard library, and the same exported symbols. In these cases, the PR implemented the Python manylinux and clang conventions for plugins. CMake targets also changed considerably because they have many new, very specific visibility rules depending on their use cases.
All of these moving parts need to be maintained, and we need checks to ensure they won’t break in the future on any platform. For instance, getting a single MRDOCS_DECL or MRDOCS_VISIBLE wrong would break these invariants. For this reason, this PR also includes lots of linters and checks to make sure this doesn’t break (the PR uses Mr.Docs itself for this visibility check, so we found another use case). All invariants need to hold on every platform, with both scripting engines, in static and shared builds, and on old and new Linux distributions. Each of those became a rule with a check that fails CI when it does.
Changes
A plugin now loads only into the build it was compiled against, and scripts and plugins share one pipeline and one reporter (competing global symbols don’t work well when you load plugins). The changes were significant enough that the documentation and contributor guide had to be updated and reorganized, and the new rules needed new linters and tests:
Features
The main feature is complete native plugins that load only into the build they target
registerTransformandregisterGeneratorthat can take an id and a callable. The scripts only need a proxy of the session object now so this code has been simplified.mrdocs_add_pluginamong other functions. This function can be used like the analogous cmake functions to create libraries. It creates a mr.docs plugin and set all appropriate visibility tags or any other requirements for the platform.dlopen/LoadLibraryExloader (never unloaded) and fixes to file reads for UTF-8 paths on Windows.MRDOCS_SOURCEhas to be corrected and was also renamed; the functionsregisterTransformandregisterGeneratorare part of the plugin context.MRDOCS_DYNAMIC_LINKmarks code that reaches MrDocs through another module, while the way to distinguish the conditions for __declspec(dllexport) changed because, unlike Boost libraries (where we got the pattern from), Mr.Docs needs __declspec(dllexport) on the static build as well.mrdocs_add_plugin`) so that there are no conflicts when the plugin is loaded into the same process. Otherwise, the wrong code would be executed. Also in CMake, we implemented a system where the export table is decided entirely by theMRDOCS_DECLmarkers. This is hard to achieve, and many tools resort to globs or just accept the errors. In our case, we used Mr.Docs itself to ensure we never get this wrong with an extension for export markers. The PR also provides the abstraction for shared builds to be able to load plugins with the exact same recipes, and the whole logic is adjusted under the hood.mrdocs::plugin-api, which is a target that includes the headers with exported symbols but not definitions, because these come only once from the running process. Each class carries exactly the export marker its members need, considering the difference between platforms where we needMRDOCS_VISIBLEfor type information only on Unix and Windows doesn’t allow visibility with duplicate sources.Build
Fixes
Refactors
As part of the new requirements coming from linters and limitations of plugins, we had to refactor part of the code. We have a session object now that owns its reporter and other classes related to the mr.docs engine. We also joined the pipelines for script extensions and native extensions into the same pipeline and documented them as a single thing.
ctxis built next to its caller now. In C++, the generators and transforms can also be defined by lambdas or classes and be wrapped like the plugins do.Some smaller refactors came from hardened guards the code asked for as we had to review this long diff.
reformat.pyhonours--checkand takes the files and lines to format.clang-formatwas updated again to describe the code as written a little closer.Documentation
In the documentation:
Continuous Integration
In CI, we enforce release floors for Linux and macOS, shared rows, and the release checks. This is what ensures that our releases are going to work on other versions of these operating systems. For the Linux release, we have a glibc floor and a bundled C++ runtime to ensure Mr.Docs uses the same one it was built with, and then plugins can be loaded smoothly (same Python manylinux strategy). For macOS 14, we set a minimum for the release, and its LLVM and the macOS release check fails when the deployment target is not set.
Examples and tests
Examples and test suites were reorganized.
examples/extensions. The stats generator and brief-filler transform are separate plugins now. We now have one directory per example with every variant (JS/Lua/C++). We create a load-and-run example for the library page. Each example has its own tests and one addon root per language.tests/integration. In theintegration/plugin-apitests, the cases are grouped into exports, refused, dependencies, and reload. The CMake consumer tests intests/integration/cmaketest Mr.Docs as executable, plugin, and library parts.Breaking changes
ctx.report.warn,ctx.report.errorandctx.report.infoinstead of the globalmrdocs.report.ctxexists only during those calls.Sessionis made withSession::create(config), which takes the configuration by value and returns anExpected<Session>. The session owns the configuration, andsession.config()returns it.Corpus::buildtakes the reporter asreport::Reporter&instead of astd::shared_ptr, and the reporter has to outlive the corpus.session.log()returns areport::Reporter&, sosession.log()->warn(...)becomessession.log().warn(...).Testing
Integration tests cover refused plugins, symbol isolation, dependencies, reloading and a consumer built with another compiler, and CI runs them on every compiler, static and shared.
Documentation
The plugin pages explain what must match and how to build a plugin, and the contributor guide documents each rule and the check that guards it.
Closes #1256
Fixes #58