Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 61 additions & 0 deletions .agents/docs/2026-10-28-add-fribidi-mwrap-plan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# Add mcpplibs.mwrap.fribidi (fribidi-m 1.0.16)

## Form

- Third-party upstream [fribidi/fribidi](https://github.com/fribidi/fribidi), release 1.0.16, LGPL-2.1-or-later.
The tarball (`1b1cde5b…a9ea05c`) is the one compat.fribidi 1.0.16 already pins.
- Form A external mwrap module repository [mcpplibs/fribidi-m](https://github.com/mcpplibs/fribidi-m), package
`mcpplibs.mwrap.fribidi`, version 1.0.16 (first release of the identity, no revision suffix), tag series
`mwrap-${version}`.
- Implementation ownership: the library is compat.fribidi's (a static library; its configuration and ABI are
sufficient). The wrapper vendors no upstream code. It retains upstream's README.md, COPYING, AUTHORS and the
Unicode Character Database notice of the data its tables come from under `upstream/`, with checksums in
`upstream/documents.json`, and deploys them with its own LICENSE/NOTICE (`runtime.deploy`): the LGPL travels
with every program that links FriBidi statically.

## Modules

- `mcpplibs.mwrap.fribidi`: every type, enumerator (FriBidiCharSet, the joining types), FRIBIDI_ENTRY function
(32, deprecated included) and variable of the headers upstream installs, by `export using`, generated from the
pinned headers as a C++ translation unit sees them (`FriBidiCharType`/`FriBidiParType` are `uint32_t` in C++).
- `fribidi_debug_status` / `fribidi_set_debug` are declared by `fribidi-common.h` outside its `extern "C"` block,
so a C++ call through the header does not link. They are adapted under their names in `mcpplibs::mwrap::fribidi`,
calling the library's C functions.
- `mcpplibs.mwrap.fribidi.macros`: 171 object-like macros as `inline constexpr` of the type of their C++ expansion,
55 function-like macros as constexpr function templates whose body is the macro itself (value, type and value
category of the macro for every argument type; assigning macros take their argument by reference).
- Stay macros, in `<mwrap/fribidi/macros.hpp>` (= `<fribidi.h>`, with `FRIBIDI_LIB_STATIC` on Windows as the
module has it): declaration attributes, the `extern "C"` brackets, the deprecation pragmas and the X-macro
protocol of the `*-list.h` tables. Not mapped: the 16 include guards.

`tools/check_exports.py` fails when the generated sources are stale, a declaration is neither exported nor
adapted, or a macro is neither mapped nor listed; every mapping is checked against the raw header in a
header-mixing test.

## Verification

Wrapper CI (Linux GCC 16.1 and LLVM 23.1.3, macOS LLVM, Windows LLVM, all required): build, the four test
programs, the export audit, and FriBidi's whole test suite on `examples/fribidi` (upstream's `fribidi` program
and its two conformance runners, ported to the module): the six charset tests byte for byte, BidiTest.txt
(770,241 cases) and BidiCharacterTest.txt (91,707 cases), nothing excluded. The templates job scaffolds the
default `bidi` template as an installed package and runs it.

Here, with the CI-pinned mcpp 2026.10.26.1 and `MCPP_INDEX_MIRROR=GLOBAL`:

- `bash tests/run_members.sh fribidi-bidi`: the real release archive through the candidate index; paragraph
levels with bracket pairs, log2vis with mirroring, Arabic shaping, the macros, and the side header beside the
import with an X-macro table: `test result ok`.
- `python3 tests/mwrap/check_installed_template.py --package mcpplibs.mwrap.fribidi@1.0.16 --deploy`: the
unmodified template from the real archive, then a relocated self-contained bundle whose `licenses/fribidi/`
holds the wrapper's LICENSE/NOTICE and FriBidi's COPYING, AUTHORS, README and UCD notice byte for byte.

## Mirror and updates

No `mcpp-res` access in this change, so the descriptor uses the GitHub release URL as a plain string (CN users
fall back to it); a maintainer can add the GitCode mirror with identical bytes. `extensions.autoupdate`
follows the repository's own `mwrap-${version}` tags.

## Motivation

openmui's text engine (openmui.text) computes bidi levels with FriBidi; through this module its native unit
imports the library instead of including C headers.
5 changes: 5 additions & 0 deletions .github/workflows/validate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1623,6 +1623,11 @@ jobs:
shell: bash
run: python3 tests/mwrap/check_installed_template.py --mcpp "$MCPP" --package mcpplibs.mwrap.libpng@1.6.43

- name: Installed FriBidi wrapper archive, actual default bidi template and its relocated LGPL notices
if: contains(format(' {0} ', env.MEMBERS), ' fribidi-bidi ') && matrix.toolchain == 'default'
shell: bash
run: python3 tests/mwrap/check_installed_template.py --mcpp "$MCPP" --package mcpplibs.mwrap.fribidi@1.0.16 --deploy

- name: Installed Godot wrapper archive and actual standalone math template
if: contains(format(' {0} ', env.MEMBERS), ' godot-math ') && matrix.toolchain == 'default'
shell: bash
Expand Down
1 change: 1 addition & 0 deletions mcpp.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ members = [
"tests/mwrap/lua-embed",
"tests/mwrap/quickjs-embed",
"tests/mwrap/libpng-image",
"tests/mwrap/fribidi-bidi",
"tests/mwrap/grpc-cpu",
"tests/mwrap/grpc-tools",
"tests/mwrap/godot-math",
Expand Down
42 changes: 42 additions & 0 deletions pkgs/mwrap/fribidi.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
-- Immutable upstream-aligned mwrap source release (fribidi-m mwrap-1.0.16).
-- The CN mirror is added by a maintainer with mcpp-res access; until then CN
-- users fall back to the GitHub release (docs/cn-mirror.md).
--
-- The wrapper is Apache-2.0; the library it links, compat.fribidi, stays
-- LGPL-2.1-or-later and is linked statically: the package deploys FriBidi's
-- COPYING, AUTHORS and README beside programs (runtime.deploy).
package = {
spec = "1",
namespace = "mcpplibs.mwrap",
name = "fribidi",
description = "fribidi-m: GNU FriBidi (the Unicode Bidirectional Algorithm) as C++23 modules",
licenses = { "Apache-2.0" },
repo = "https://github.com/mcpplibs/fribidi-m",
type = "package",
extensions = {
autoupdate = {
tag = "mwrap-${version}",
url = "https://github.com/mcpplibs/fribidi-m/releases/download/${tag}/fribidi-m-${version}.tar.gz",
},
},
xpm = {
linux = {
["1.0.16"] = {
url = "https://github.com/mcpplibs/fribidi-m/releases/download/mwrap-1.0.16/fribidi-m-1.0.16.tar.gz",
sha256 = "e05d6f3407689e8372a9876c1a0089df38cfbe33a7df9174ef80c134b7b198f4",
},
},
macosx = {
["1.0.16"] = {
url = "https://github.com/mcpplibs/fribidi-m/releases/download/mwrap-1.0.16/fribidi-m-1.0.16.tar.gz",
sha256 = "e05d6f3407689e8372a9876c1a0089df38cfbe33a7df9174ef80c134b7b198f4",
},
},
windows = {
["1.0.16"] = {
url = "https://github.com/mcpplibs/fribidi-m/releases/download/mwrap-1.0.16/fribidi-m-1.0.16.tar.gz",
sha256 = "e05d6f3407689e8372a9876c1a0089df38cfbe33a7df9174ef80c134b7b198f4",
},
},
},
}
19 changes: 19 additions & 0 deletions tests/mwrap/check_installed_template.py
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,25 @@ def main():
assert copied.is_file(), f"Missing retained OpenCV notice: {copied}"
assert copied.read_bytes() == notice.read_bytes(), copied
print(f"Verified {len(notices)} byte-identical wrapper/upstream license files", flush=True)
elif identity == "mcpplibs.mwrap.fribidi":
# FriBidi is LGPL-2.1-or-later and linked statically: its license,
# authors and README travel with the program, beside the wrapper's.
# The installed archive in the test home's package store.
sources = [path for store in (home / "xpkgs", home / "registry/data/xpkgs")
for path in store.glob(f"mcpplibs.mwrap-x-fribidi/{version}/*/src/fribidi.cppm")]
assert len(sources) == 1, sources
source = sources[0].parent.parent
declared = tomllib.loads((source / "mcpp.toml").read_text(encoding="utf-8"))["runtime"]["deploy"]
roots = list(relocated.rglob("licenses/fribidi"))
assert len(roots) == 1, f"Missing FriBidi runtime license tree: {roots}"
for entry in declared:
notice = source / entry["from"]
copied = roots[0].parent.parent / entry["to"] / notice.name
assert copied.is_file() and copied.read_bytes() == notice.read_bytes(), copied
assert b"GNU LESSER GENERAL PUBLIC LICENSE" in (roots[0] / "upstream/fribidi/COPYING").read_bytes()
assert b"Apache License" in (roots[0] / "LICENSE").read_bytes()
assert b"mcpp-community" in (roots[0] / "NOTICE").read_bytes()
print(f"Verified {len(declared)} byte-identical wrapper/FriBidi license files", flush=True)
else:
raise ValueError(f"No deployment acceptance contract for {identity}")
runtime_env = {key: value for key, value in os.environ.items()
Expand Down
11 changes: 11 additions & 0 deletions tests/mwrap/fribidi-bidi/mcpp.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
[package]
name = "fribidi-bidi"
version = "1.0.16"
description = "Consume the immutable FriBidi wrapper: paragraph levels with bracket pairs, line reordering, log2vis, Arabic shaping, the mapped macros, the side header beside the import"

[indices]
# Use the complete candidate built-in index prepared by tests/run_members.sh.
# An explicit empty table overrides the legacy workspace redirect.

[dependencies]
"mcpplibs.mwrap.fribidi" = "1.0.16"
57 changes: 57 additions & 0 deletions tests/mwrap/fribidi-bidi/tests/behavior.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import std;
import mcpplibs.mwrap.fribidi;

using namespace mcpplibs::mwrap::fribidi::macros;

namespace {
std::vector<FriBidiChar> text(std::u32string_view s) { return {s.begin(), s.end()}; }
std::u32string chars(const std::vector<FriBidiChar>& v) {
std::u32string out;
for (auto c : v) out.push_back(static_cast<char32_t>(c));
return out;
}
} // namespace

int main() {
// UAX #9 N0's example in an RTL paragraph: a bracket pair resolves together.
auto s = text(U"אב(גד[&ef]!)gh");
const auto n = static_cast<FriBidiStrIndex>(s.size());
std::vector<FriBidiCharType> types(s.size());
std::vector<FriBidiBracketType> brackets(s.size());
std::vector<FriBidiLevel> levels(s.size());
fribidi_get_bidi_types(s.data(), n, types.data());
fribidi_get_bracket_types(s.data(), n, types.data(), brackets.data());
FriBidiParType base = FRIBIDI_PAR_RTL;
if (fribidi_get_par_embedding_levels_ex(types.data(), brackets.data(), n, &base, levels.data()) != 3) return 1;
if (std::vector<int>(levels.begin(), levels.end()) != std::vector<int>{1, 1, 1, 1, 1, 1, 1, 2, 2, 1, 1, 1, 2, 2}) return 2;

// A line to visual order, with mirrored brackets.
auto logical = text(U"abc אב(ג) def");
const auto len = static_cast<FriBidiStrIndex>(logical.size());
std::vector<FriBidiChar> visual(logical.size());
FriBidiParType dir = FRIBIDI_PAR_ON;
if (fribidi_log2vis(logical.data(), len, &dir, visual.data(), nullptr, nullptr, nullptr) == 0) return 3;
if (dir != FRIBIDI_PAR_LTR || chars(visual) != U"abc (ג)בא def") return 4;

// Arabic joining and shaping to presentation forms.
auto arabic = text(U"بب");
std::vector<FriBidiCharType> atypes(2);
std::vector<FriBidiLevel> alevels(2);
std::vector<FriBidiArabicProp> props(2);
fribidi_get_bidi_types(arabic.data(), 2, atypes.data());
FriBidiParType abase = FRIBIDI_PAR_ON;
if (!fribidi_get_par_embedding_levels_ex(atypes.data(), nullptr, 2, &abase, alevels.data())) return 5;
fribidi_get_joining_types(arabic.data(), 2, props.data());
fribidi_join_arabic(atypes.data(), 2, alevels.data(), props.data());
fribidi_shape(FRIBIDI_FLAGS_DEFAULT | FRIBIDI_FLAGS_ARABIC, alevels.data(), 2, props.data(), arabic.data());
if (chars(arabic) != U"\uFE91\uFE90") return 6;

// Macros as constants and constexpr functions; the adapted debug function.
static_assert(FRIBIDI_IS_RTL(FRIBIDI_TYPE_AL) && FRIBIDI_DIR_TO_LEVEL(FRIBIDI_PAR_RTL) == 1);
static_assert(std::string_view(FRIBIDI_VERSION) == "1.0.16");
if (mcpplibs::mwrap::fribidi::fribidi_set_debug(0) != 0) return 7;
if (std::string_view(fribidi_unicode_version) != "16.0.0") return 8;
if (fribidi_parse_charset("UTF-8") != FRIBIDI_CHAR_SET_UTF8) return 9;
std::println("fribidi-bidi: levels, reordering, shaping and macros ok");
return 0;
}
20 changes: 20 additions & 0 deletions tests/mwrap/fribidi-bidi/tests/side_header.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// The side header from the installed package, beside the import: the raw
// macros and the X-macro tables with the module's entities.
#include <mwrap/fribidi/macros.hpp>

import std;
import mcpplibs.mwrap.fribidi;

constexpr std::pair<FriBidiCharSet, const char*> sets[] = {
#define _FRIBIDI_ADD_CHAR_SET(CHAR_SET, char_set) {FRIBIDI_CHAR_SET_##CHAR_SET, #char_set},
#include <fribidi-char-sets-list.h>
#undef _FRIBIDI_ADD_CHAR_SET
};

int main() {
if (std::size(sets) != static_cast<std::size_t>(FRIBIDI_CHAR_SETS_NUM)) return 1;
for (auto [set, name] : sets)
if (fribidi_parse_charset(fribidi_char_set_name(set)) != set) return 2;
if (FRIBIDI_IS_RTL(::fribidi_get_bidi_type(0x05D0)) != (mcpplibs::mwrap::fribidi::macros::FRIBIDI_IS_RTL)(FRIBIDI_TYPE_RTL)) return 3;
return 0;
}
Loading