From 91d0be4bbf3bfe897033824cedadf9bca3201153 Mon Sep 17 00:00:00 2001 From: Cloud_Yun Date: Wed, 2 Sep 2026 04:16:35 +0900 Subject: [PATCH] feat(ftxui): a `modules` feature for 7.0.3's named modules MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit FTXUI 7 ships upstream's own module units — an `ftxui` umbrella re-exporting ftxui.component/.dom/.screen/.util — and #292 left them out after an unconditional attempt went red on the linux gcc leg. They come back here behind an opt-in `modules` feature whose source list is exactly upstream's cmake/ftxui_modules.cmake: five .cppm, a pure addition that cannot collide with the base **/*.cpp globs, so the default build does not move (76 compiled units without the feature, 81 with). What that gcc leg was reporting is narrower than "GCC 16 cannot consume these modules". The sub-modules put the public headers, and transitively libstdc++, into a global module fragment, so a consumer TU that writes `import ftxui;` AND textually #includes a standard header hands gcc two copies of the standard library and it refuses — redefinition of std::__terminate, conflicting declaration of std::allocator / std::char_traits. #292's smoke TU did exactly that, and the only object that failed there was that TU's own (obj/module.o), after the package's five module units had already compiled. clang accepts the mixed TU, which is why only gcc went red. Kept off the textual surface, the units build and run on gcc 16.1.0 and llvm 22.1.8 alike, so tests/examples/ftxui-module needs no toolchain pin and runs on both linux legs: `import ftxui;` with a real render, plus each sub-module imported alone, every TU on the module surface and not one textual include. The `modules` export list is documentation, not a guard: mcpp validates [modules].exports only for a build's PRIMARY manifest, never for a dependency's — measured by deleting one name and rebuilding with the package cache bypassed, which passed. Verified (mcpp 2026.8.27.2, the version CI pins): ftxui-module 5 passed on both toolchains; `mcpp test -p core` green; the same import with the feature off fails with `module 'ftxui' not found`; 6.1.9 and the 7.0.3 header path unaffected; check_mirror_urls, check_package_name, check_platform_version_parity, check_duplicate_versions, check_cross_package_refs and `mcpp xpkg parse` over all 175 descriptors green. Co-Authored-By: Claude Fable 5 --- mcpp.toml | 1 + pkgs/c/compat.ftxui.lua | 100 +++++++++++++++++- tests/examples/ftxui-module/mcpp.toml | 28 +++++ .../ftxui-module/tests/sub_component.cpp | 21 ++++ tests/examples/ftxui-module/tests/sub_dom.cpp | 16 +++ .../ftxui-module/tests/sub_screen.cpp | 26 +++++ .../examples/ftxui-module/tests/sub_util.cpp | 26 +++++ .../examples/ftxui-module/tests/umbrella.cpp | 18 ++++ 8 files changed, 235 insertions(+), 1 deletion(-) create mode 100644 tests/examples/ftxui-module/mcpp.toml create mode 100644 tests/examples/ftxui-module/tests/sub_component.cpp create mode 100644 tests/examples/ftxui-module/tests/sub_dom.cpp create mode 100644 tests/examples/ftxui-module/tests/sub_screen.cpp create mode 100644 tests/examples/ftxui-module/tests/sub_util.cpp create mode 100644 tests/examples/ftxui-module/tests/umbrella.cpp diff --git a/mcpp.toml b/mcpp.toml index e51e86f..2556703 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -45,6 +45,7 @@ members = [ "tests/examples/ffmpeg", "tests/examples/ffmpeg-module", "tests/examples/fmtlib.fmt", + "tests/examples/ftxui-module", "tests/examples/gmp", "tests/examples/gmp-gmpxx", "tests/examples/godot-cpp", diff --git a/pkgs/c/compat.ftxui.lua b/pkgs/c/compat.ftxui.lua index 2e1f7c9..4b31232 100644 --- a/pkgs/c/compat.ftxui.lua +++ b/pkgs/c/compat.ftxui.lua @@ -1,6 +1,7 @@ -- M6.x glob-aware Form B descriptor for FTXUI 6.1.9 and 7.0.3. -- --- Pure C++ library (no C++23 modules); compiled sources + public headers. +-- Compiled sources + public headers by default; 7.0.3 additionally offers +-- upstream's named modules behind the opt-in `modules` feature (see below). -- Uses mcpp 0.0.4's glob exclusion (`!` prefix) to skip the -- *_test.cpp / *_fuzzer.cpp files that live alongside the library -- sources in the same directories (6.1.9: ~30 test / ~16 fuzzer; @@ -14,6 +15,69 @@ -- FTXUI_BUILD_MODULES, off by default); the `*.cpp` globs never match them, -- and the plain .cpp sources still compile header-only style. -- +-- The `modules` feature (7.0.3+) +-- ------------------------------ +-- 7.x ships upstream's own named modules — an `ftxui` umbrella that +-- `export import`s four sub-modules (ftxui.component/.dom/.screen/.util), +-- each of which textually includes the matching public headers in its global +-- module fragment. `features.modules` adds exactly the five files upstream's +-- cmake/ftxui_modules.cmake lists, so `import ftxui;` becomes available +-- without touching the header surface: the module units carry no definitions +-- of their own (they are `export namespace ftxui { using ... }` re-exports), +-- so they layer ON TOP of the same libftxui.a the default build produces. +-- Both surfaces coexist in one archive; a consumer picks either. +-- +-- OFF BY DEFAULT — but NOT because the units fail to build. They build and +-- run under gcc 16.1.0 and llvm 22.1.8 alike, verified on the mcpp version CI +-- pins. The reason is cost and choice: the module surface is five extra TUs +-- and their BMIs that no header consumer of 7.0.3 should pay for unasked, +-- upstream itself defaults FTXUI_BUILD_MODULES to OFF, and there is a +-- consumer-side constraint below that only the consumer can honour. +-- +-- ⚠️ THE CONSTRAINT IS IN THE CONSUMER'S TU, NOT IN THIS PACKAGE, AND #292 +-- IS WHY IT IS WORTH SPELLING OUT. Because each sub-module puts the public +-- headers — and transitively libstdc++ — into a global module fragment, a +-- consumer TU that writes `import ftxui;` and ALSO textually `#include`s a +-- standard header hands gcc two copies of the standard library's +-- declarations. gcc 16 refuses, at volume: +-- +-- c++config.h:355:15: error: redefinition of 'void std::__terminate()' +-- memoryfwd.h:68:11: error: conflicting declaration of template +-- 'template struct std::allocator' +-- stringfwd.h:55:12: error: conflicting declaration of template +-- 'template struct std::char_traits' +-- +-- That is exactly what sank #292's first attempt at this. Its smoke TU wrote +-- `import ftxui;` above `#include ` and `#include `, +-- and the linux gcc leg died on those three errors (and ~16k more) while +-- llvm, macOS and windows stayed green — clang accepts the mixed TU. The one +-- object that failed in that run was the smoke TU's own (`obj/module.o`); the +-- package's five module units had already compiled. +-- +-- A consumer that stays ON the module surface — `import std;` beside +-- `import ftxui;`, no textual includes, the discipline tests/examples/ +-- asio-module already documents — builds clean on both compilers. That is +-- what tests/examples/ftxui-module asserts, and it is why that member can run +-- on both of CI's linux legs rather than needing a toolchain pin. +-- +-- Upstream's own module CI is llvm-only (`test_modules` in +-- .github/workflows/build.yaml: a one-entry ubuntu + llvm matrix carrying +-- `# TODO add gcc / msvc`), and ftxui_modules.cmake still forces +-- `-fmodules-ts` under CMAKE_COMPILER_IS_GNUCXX above a bare +-- `# TODO: Explain why this is needed.`. So gcc is UNTESTED upstream — worth +-- knowing before trusting the combination far — but, as measured here, it is +-- not broken. +-- +-- On 6.1.9 the feature's glob matches nothing (no .cppm before 7.0.0), which +-- is a warning rather than an error — the same union-of-layouts tolerance +-- compat.catch2 and compat.redis-plus-plus rely on. `modules` below is the +-- declared export set, in the same spelling every other module package in +-- this index uses. Note what it does NOT buy here: mcpp validates +-- `[modules].exports` against the scanner only for the PRIMARY manifest of a +-- build, so a DEPENDENCY's list is never checked — measured by deleting +-- `ftxui.util` from it and rebuilding with the package cache bypassed, which +-- built and passed. It is documentation and metadata, not a guard. +-- -- ONE version skew the globs cannot express (no per-version build blocks, -- mcpp-community/mcpp#290): FTXUI 7 moved Loop's method definitions from -- loop.cpp into app.cpp and dropped loop.cpp from the CMake build, but the @@ -98,6 +162,40 @@ package = { "!*/src/ftxui/**/*_fuzzer.cpp", -- fuzz targets (16 in 6.1.9, 6 in 7.0.3) }, targets = { ["ftxui"] = { kind = "lib" } }, + -- The export set of the `modules` feature, in the spelling every other + -- module package in this index uses. Documentation and metadata only: + -- mcpp checks `[modules].exports` against the scanner for the PRIMARY + -- manifest of a build, never for a dependency's, so nothing here is + -- enforced at a consumer's build (measured — see the header comment). + -- Order follows upstream's ftxui_modules.cmake. + modules = { + "ftxui", + "ftxui.component", + "ftxui.dom", + "ftxui.screen", + "ftxui.util", + }, + features = { + -- Upstream's five module units, verbatim from + -- cmake/ftxui_modules.cmake. `*.cppm` cannot collide with the base + -- `**/*.cpp` globs (different extension), so this is a pure + -- ADDITION — no `!` exclusion is involved and the base source set + -- is untouched whether the feature is on or off. That matters: + -- a `!` exclusion in mcpp is global and would out-rank a feature + -- entry naming the same file, so "exclude in base, add back in the + -- feature" is not an expressible shape. + -- + -- No `include_dirs` here (features cannot carry them, and none is + -- needed): the GMF `#include ` resolve through the + -- package-level `*/include`, which mcpp applies to the package's + -- own TUs as well as to consumers. + -- + -- Consumer-side rule, gcc only: don't mix `import ftxui;` with a + -- textual `#include` in one TU. See the header comment. + ["modules"] = { + sources = { "*/src/ftxui/*.cppm" }, + }, + }, deps = { }, windows = { cxxflags = { "-DUNICODE", "-D_UNICODE" }, diff --git a/tests/examples/ftxui-module/mcpp.toml b/tests/examples/ftxui-module/mcpp.toml new file mode 100644 index 0000000..09d97e1 --- /dev/null +++ b/tests/examples/ftxui-module/mcpp.toml @@ -0,0 +1,28 @@ +# compat.ftxui's `modules` feature: upstream's own named modules for 7.0.3 — +# the `ftxui` umbrella plus ftxui.component/.dom/.screen/.util. The sibling +# member tests/examples/core covers the default HEADER surface at the same +# version and requests no feature; this member exists to prove the feature. +# +# Part of the self-referential workspace: the dependency resolves to the +# checked-in descriptor (pkgs/c/compat.ftxui.lua) through the workspace-root +# `[indices]` redirect, which this member inherits. +# +# ⚠️ EVERY TU HERE STAYS ON THE MODULE SURFACE — `import std;` beside +# `import ftxui...;`, and not one textual `#include`. That is not style, it is +# the thing the member is guarding. A consumer TU that mixes `import ftxui;` +# with a textual `#include` of a standard header hands gcc two copies of +# libstdc++'s declarations through the sub-modules' global module fragments, +# and gcc 16 rejects it with thousands of `redefinition of 'void +# std::__terminate()'` / `conflicting declaration of template ... std::allocator` +# errors. That is what took down #292's first attempt at compiling these units, +# and it is a CONSUMER-side constraint, not a defect in the package. clang +# accepts the mixed TU, which is why only the linux gcc leg went red there. +# +# Kept unmixed, the feature builds and runs on gcc 16.1.0 and llvm 22.1.8 +# alike, so this member needs no toolchain pin and runs on both linux legs. +[package] +name = "ftxui-module-tests" +version = "0.1.0" + +[dependencies.compat] +ftxui = { version = "7.0.3", features = ["modules"] } diff --git a/tests/examples/ftxui-module/tests/sub_component.cpp b/tests/examples/ftxui-module/tests/sub_component.cpp new file mode 100644 index 0000000..3c3ffd0 --- /dev/null +++ b/tests/examples/ftxui-module/tests/sub_component.cpp @@ -0,0 +1,21 @@ +// ftxui.component alone: Component/Event/Button and event routing. +import std; +import ftxui.component; + +int main() { + using namespace ftxui; + int clicked = 0; + Component button = Button("go", [&] { ++clicked; }); + + Component container = Container::Vertical({button}); + if (!container->OnEvent(Event::Return)) return 1; + if (clicked != 1) return 2; + + const Event a = Event::Character('a'); + if (!a.is_character() || a.character() != "a") return 3; + if (Event::Return == a) return 4; + + std::println("clicked={}", clicked); + std::println("ftxui.component OK"); + return 0; +} diff --git a/tests/examples/ftxui-module/tests/sub_dom.cpp b/tests/examples/ftxui-module/tests/sub_dom.cpp new file mode 100644 index 0000000..0e3fde6 --- /dev/null +++ b/tests/examples/ftxui-module/tests/sub_dom.cpp @@ -0,0 +1,16 @@ +// ftxui.dom alone: elements + Render + Screen, no umbrella, no headers. +import std; +import ftxui.dom; + +int main() { + using namespace ftxui; + Element document = vbox({text("dom-only"), separator(), text("row2")}); + auto screen = Screen::Create(Dimension::Fit(document), Dimension::Fit(document)); + Render(screen, document); + const std::string rendered = screen.ToString(); + std::println("dom rendered: [{}]", rendered); + if (rendered.find("dom-only") == std::string::npos) return 1; + if (rendered.find("row2") == std::string::npos) return 2; + std::println("ftxui.dom OK"); + return 0; +} diff --git a/tests/examples/ftxui-module/tests/sub_screen.cpp b/tests/examples/ftxui-module/tests/sub_screen.cpp new file mode 100644 index 0000000..d1a57c2 --- /dev/null +++ b/tests/examples/ftxui-module/tests/sub_screen.cpp @@ -0,0 +1,26 @@ +// ftxui.screen alone: Screen/Pixel/Color/Terminal and the _rgb literal. +import std; +import ftxui.screen; + +int main() { + using namespace ftxui; + auto screen = Screen::Create(Dimensions{4, 2}); + screen.PixelAt(0, 0).character = "X"; + screen.PixelAt(3, 1).character = "Y"; + const std::string s = screen.ToString(); + std::println("screen: [{}]", s); + if (s.find('X') == std::string::npos) return 1; + if (s.find('Y') == std::string::npos) return 2; + + const Color red = Color::Red; + const Color rgb = Color::RGB(1, 2, 3); + if (red == rgb) return 3; + using namespace ftxui::literals; + const Color lit = 0x0102ff_rgb; + if (lit == red) return 4; + + if (string_width("abc") != 3) return 5; + if (to_string(to_wstring(std::string("mcpp"))) != "mcpp") return 6; + std::println("ftxui.screen OK"); + return 0; +} diff --git a/tests/examples/ftxui-module/tests/sub_util.cpp b/tests/examples/ftxui-module/tests/sub_util.cpp new file mode 100644 index 0000000..b7bbcc4 --- /dev/null +++ b/tests/examples/ftxui-module/tests/sub_util.cpp @@ -0,0 +1,26 @@ +// ftxui.util alone: Ref/ConstRef/StringRef and AutoReset. +import std; +import ftxui.util; + +int main() { + using namespace ftxui; + int backing = 7; + Ref r(&backing); + *r = 9; + if (backing != 9) return 1; + + ConstRef cr(5); + if (*cr != 5) return 2; + + std::string text = "abc"; + StringRef sr(&text); + *sr = "xyz"; + if (text != "xyz") return 3; + + int guarded = 1; + { AutoReset reset(&guarded, 42); if (guarded != 42) return 4; } + if (guarded != 1) return 5; + + std::println("ftxui.util OK"); + return 0; +} diff --git a/tests/examples/ftxui-module/tests/umbrella.cpp b/tests/examples/ftxui-module/tests/umbrella.cpp new file mode 100644 index 0000000..e39fd69 --- /dev/null +++ b/tests/examples/ftxui-module/tests/umbrella.cpp @@ -0,0 +1,18 @@ +// `import ftxui;` — the umbrella module — plus a real dom render. +import std; +import ftxui; + +int main() { + using namespace ftxui; + Element document = hbox({text("compat"), separator(), text("ftxui")}); + auto screen = Screen::Create(Dimension::Fit(document), Dimension::Fit(document)); + Render(screen, document); + const std::string rendered = screen.ToString(); + std::println("rendered: [{}]", rendered); + if (rendered.find("compat") == std::string::npos) return 1; + if (rendered.find("ftxui") == std::string::npos) return 2; + // the separator must actually have drawn something between them + if (rendered.find("compat") > rendered.find("ftxui")) return 3; + std::println("umbrella OK"); + return 0; +}