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; +}