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
54 changes: 54 additions & 0 deletions .agents/docs/2026-10-28-add-sheenbidi-mwrap-plan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# Add mcpplibs.mwrap.sheenbidi (sheenbidi-m 3.0.0)

## Form

- Third-party upstream [Tehreer/SheenBidi](https://github.com/Tehreer/SheenBidi), release v3.0.0 (Unicode 17.0.0),
Apache-2.0. The tag archive (`86c56014…f786`) is the one compat.sheenbidi 3.0.0 pins (the parent change).
- Form A external mwrap module repository [mcpplibs/sheenbidi-m](https://github.com/mcpplibs/sheenbidi-m), package
`mcpplibs.mwrap.sheenbidi`, version 3.0.0 (first release of the identity, no revision suffix), tag series
`mwrap-${version}`; release asset `sheenbidi-m-3.0.0.tar.gz` (`76a0b0fe…3c41`), CN mirror
`gitcode.com/mcpp-res/mwrap-sheenbidi` with identical bytes.
- Implementation ownership: the library is compat.sheenbidi's (a static library from upstream's unity source; its
configuration and ABI are sufficient). The wrapper vendors no upstream code. It retains upstream's README.md and
LICENSE under `upstream/sheenbidi/` and the Unicode License v3 under `upstream/unicode/`, with checksums in
`upstream/documents.json`, and deploys them with its own LICENSE/NOTICE (`runtime.deploy`).

## Modules

- `mcpplibs.mwrap.sheenbidi`: every type (32), struct tag (11) and `SB_PUBLIC` function (51) of the headers upstream
installs, by `export using`, generated from the pinned headers. The experimental text API is compiled out
(compat.sheenbidi does not define `SB_CONFIG_EXPERIMENTAL_TEXT_API`, as upstream's default builds do not).
- `mcpplibs.mwrap.sheenbidi.macros`: 12 object-like macros as `inline constexpr` of the type of their expansion, 16
function-like macros as constexpr function templates whose body is the macro, and the 236 enumerators of the
headers' unnamed enums (TU-local, so not exportable as they are) as constants of the typedef the header gives their
values (`SBBidiType`, `SBGeneralCategory`, `SBScript`, `SBStringEncoding`, `SBBoolean`).
- Stay macros, in `<mwrap/sheenbidi/macros.hpp>` (= `<SheenBidi/SheenBidi.h>`): `SB_PUBLIC`, `SB_DEPRECATED`,
`SB_EXTERN_C_BEGIN` / `SB_EXTERN_C_END`. Not mapped: the 22 include guards.

`tools/check_exports.py` fails when the generated sources are stale, a declaration is not exported, an enumerator is
not mapped, 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 x86_64 GCC 16.1 and LLVM 23.1.3, Linux arm64 GCC 16.1, macOS LLVM, Windows LLVM, all required):
build, the four test programs, the export audit, and Unicode 17.0.0's conformance tests on `examples/sheenbidi`
(programs written against the module), the files taken from the release and checked byte for byte against
unicode.org's: BidiTest.txt (770,241 cases) and BidiCharacterTest.txt (91,707 cases, with the mirrored characters
against BidiMirroring.txt), 0 errors, nothing excluded. The templates job scaffolds the default `bidi` template as an
installed package and runs it. Until compat.sheenbidi is on this index's main, the wrapper's CI resolves the compat
namespace from the parent change's branch (`tools/candidate_index.py`, a no-op once published).

Here, with the CI-pinned mcpp 2026.10.26.1:

- `bash tests/run_members.sh sheenbidi-bidi`: the real release archive through the candidate index; UAX #9's N0
bracket-pair example, a line's visual order with mirrored brackets, the mirror and script locators, Unicode 17.0's
Sidetic, the macros, and the side header beside the import: `test result ok`.
- `python3 tests/mwrap/check_installed_template.py --package mcpplibs.mwrap.sheenbidi@3.0.0 --deploy`: the unmodified
template from the real archive, then a relocated self-contained bundle whose `licenses/sheenbidi/` holds the
wrapper's LICENSE/NOTICE, SheenBidi's README and LICENSE and the Unicode license byte for byte.

## Motivation

openmui's text engine computes bidi levels; SheenBidi through this module replaces FriBidi there, whose
LGPL-2.1 does not fit a statically linked, permissively licensed framework.
5 changes: 5 additions & 0 deletions .github/workflows/validate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1633,6 +1633,11 @@ jobs:
shell: bash
run: python3 tests/mwrap/check_installed_template.py --mcpp "$MCPP" --package mcpplibs.mwrap.fribidi@1.0.16 --deploy

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

- name: Installed FreeType wrapper archive, actual default banner template and its relocated FTL notices
if: contains(format(' {0} ', env.MEMBERS), ' freetype-render ') && matrix.toolchain == 'default'
shell: bash
Expand Down
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,13 @@

### Added

- **mcpplibs.mwrap.sheenbidi 3.0.0(新包)。** sheenbidi-m:以 C++23 模块(`import mcpplibs.mwrap.sheenbidi;`)
使用 compat.sheenbidi 3.0.0。导出已安装头文件的全部类型、结构体标签与 51 个 `SB_PUBLIC` 函数(上游全局名不变);
`mcpplibs.mwrap.sheenbidi.macros` 把宏映射为常量与 constexpr 函数,并把无名枚举的 236 个枚举值映射为其 typedef
类型的常量;`SB_PUBLIC`、`SB_DEPRECATED` 与 `extern "C"` 括号留在 `<mwrap/sheenbidi/macros.hpp>`。包装仓库的 CI
(Linux x86_64 GCC/LLVM、Linux arm64、macOS、Windows)运行 Unicode 17.0.0 的 BidiTest.txt(770,241 例)与
BidiCharacterTest.txt(91,707 例),0 错误。GLOBAL 为 `mwrap-3.0.0` 发布资产,CN 为 GitCode `mcpp-res/mwrap-sheenbidi`
镜像,sha256 一致。新增测试成员 `tests/mwrap/sheenbidi-bidi` 与已安装模板及其许可文件的部署检查。
- **compat.sheenbidi 3.0.0(新包)。** SheenBidi:Unicode 双向算法(UAX #9)的 C 实现,Apache-2.0,无依赖;
3.0.0 实现 Unicode 17.0.0,通过该版本的 BidiTest.txt 与 BidiCharacterTest.txt 全部用例。以上游的单一编译单元
`Source/SheenBidi.c`(`SB_CONFIG_UNITY`)从源码构建为静态库,只导出公共头文件的 51 个 `SB*` 入口;使用者的包含路径
Expand Down
1 change: 1 addition & 0 deletions mcpp.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ members = [
"tests/mwrap/libpng-image",
"tests/mwrap/concurrentqueue-mpmc",
"tests/mwrap/fribidi-bidi",
"tests/mwrap/sheenbidi-bidi",
"tests/mwrap/freetype-render",
"tests/mwrap/harfbuzz-shape",
"tests/mwrap/sdl3-headless",
Expand Down
51 changes: 51 additions & 0 deletions pkgs/mwrap/sheenbidi.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
-- Immutable upstream-aligned mwrap source release (sheenbidi-m mwrap-3.0.0);
-- GLOBAL and CN bytes are identical.
--
-- The wrapper is Apache-2.0, and so is the library it links, compat.sheenbidi
-- (SheenBidi 3.0.0, Unicode 17.0.0), linked statically: the package deploys
-- SheenBidi's README and LICENSE and the Unicode License v3 of the data its
-- tables come from beside programs (runtime.deploy).
package = {
spec = "1",
namespace = "mcpplibs.mwrap",
name = "sheenbidi",
description = "sheenbidi-m: SheenBidi (the Unicode Bidirectional Algorithm, Unicode 17.0.0) as C++23 modules",
licenses = { "Apache-2.0" },
repo = "https://github.com/mcpplibs/sheenbidi-m",
type = "package",
extensions = {
autoupdate = {
tag = "mwrap-${version}",
url = "https://github.com/mcpplibs/sheenbidi-m/releases/download/${tag}/sheenbidi-m-${version}.tar.gz",
},
},
xpm = {
linux = {
["3.0.0"] = {
url = {
GLOBAL = "https://github.com/mcpplibs/sheenbidi-m/releases/download/mwrap-3.0.0/sheenbidi-m-3.0.0.tar.gz",
CN = "https://gitcode.com/mcpp-res/mwrap-sheenbidi/releases/download/3.0.0/sheenbidi-m-3.0.0.tar.gz",
},
sha256 = "76a0b0fe0000ae43483b1b6e606908c09381f2d1cd8c736bff8b685c2fec3c41",
},
},
macosx = {
["3.0.0"] = {
url = {
GLOBAL = "https://github.com/mcpplibs/sheenbidi-m/releases/download/mwrap-3.0.0/sheenbidi-m-3.0.0.tar.gz",
CN = "https://gitcode.com/mcpp-res/mwrap-sheenbidi/releases/download/3.0.0/sheenbidi-m-3.0.0.tar.gz",
},
sha256 = "76a0b0fe0000ae43483b1b6e606908c09381f2d1cd8c736bff8b685c2fec3c41",
},
},
windows = {
["3.0.0"] = {
url = {
GLOBAL = "https://github.com/mcpplibs/sheenbidi-m/releases/download/mwrap-3.0.0/sheenbidi-m-3.0.0.tar.gz",
CN = "https://gitcode.com/mcpp-res/mwrap-sheenbidi/releases/download/3.0.0/sheenbidi-m-3.0.0.tar.gz",
},
sha256 = "76a0b0fe0000ae43483b1b6e606908c09381f2d1cd8c736bff8b685c2fec3c41",
},
},
},
}
21 changes: 21 additions & 0 deletions tests/mwrap/check_installed_template.py
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,27 @@ def main():
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)
elif identity == "mcpplibs.mwrap.sheenbidi":
# SheenBidi is Apache-2.0 and linked statically: its LICENSE and
# README, and the Unicode License v3 of the data its tables are
# generated from, travel with the program beside the wrapper's.
sources = [path for store in (home / "xpkgs", home / "registry/data/xpkgs")
for path in store.glob(f"mcpplibs.mwrap-x-sheenbidi/{version}/*/src/sheenbidi.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/sheenbidi"))
assert len(roots) == 1, f"Missing SheenBidi 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"Apache License" in (roots[0] / "upstream/sheenbidi/LICENSE").read_bytes()
assert b"UNICODE LICENSE V3" in (roots[0] / "upstream/unicode/license.txt").read_bytes()
assert b"Apache License" in (roots[0] / "LICENSE").read_bytes()
notice = (roots[0] / "NOTICE").read_bytes()
assert b"mcpp-community" in notice and b"Muhammad Tayyab Akram" in notice
print(f"Verified {len(declared)} byte-identical wrapper/SheenBidi license files", flush=True)
elif identity == "mcpplibs.mwrap.freetype":
# FreeType is linked statically under the FreeType License (FTL),
# whose credit the documentation carries: the license selector, the
Expand Down
11 changes: 11 additions & 0 deletions tests/mwrap/sheenbidi-bidi/mcpp.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
[package]
name = "sheenbidi-bidi"
version = "3.0.0"
description = "Consume the immutable SheenBidi wrapper: UAX #9's bracket-pair example in an RTL paragraph, a line's runs in visual order, the mirror and script locators, the mapped macros and enumerators, 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.sheenbidi" = "3.0.0"
52 changes: 52 additions & 0 deletions tests/mwrap/sheenbidi-bidi/tests/behavior.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import std;
import mcpplibs.mwrap.sheenbidi;

using namespace mcpplibs::mwrap::sheenbidi::macros;

int main() {
if (std::string_view(SBVersionGetString()) != "3.0.0") return 1;

// UAX #9 N0's example in an RTL paragraph: the bracket pairs resolve together.
const std::u32string s = U"אב(גד[&ef]!)gh";
const SBCodepointSequence seq{SBStringEncodingUTF32, s.data(), s.size()};
SBAlgorithmRef algorithm = SBAlgorithmCreate(&seq);
SBParagraphRef paragraph = SBAlgorithmCreateParagraph(algorithm, 0, s.size(), 1);
const SBLevel* levels = SBParagraphGetLevelsPtr(paragraph);
if (std::vector<int>(levels, levels + s.size()) != std::vector<int>{1, 1, 1, 1, 1, 1, 1, 2, 2, 1, 1, 1, 2, 2}) return 2;

// The line's runs in visual order, and its mirrored brackets.
SBLineRef line = SBParagraphCreateLine(paragraph, 0, s.size());
std::u32string visual;
const SBRun* runs = SBLineGetRunsPtr(line);
for (SBUInteger r = 0; r < SBLineGetRunCount(line); ++r)
for (SBUInteger k = 0; k < runs[r].length; ++k) {
const SBUInteger i = runs[r].level & 1 ? runs[r].offset + runs[r].length - 1 - k : runs[r].offset + k;
const SBCodepoint mirror = runs[r].level & 1 ? SBCodepointGetMirror(s[i]) : 0;
visual += static_cast<char32_t>(mirror != 0 ? mirror : s[i]);
}
if (visual != U"gh(![ef&]דג)בא") return 3;
SBMirrorLocatorRef mirrors = SBMirrorLocatorCreate();
SBMirrorLocatorLoadLine(mirrors, line, s.data());
int mirrored = 0;
while (SBMirrorLocatorMoveNext(mirrors)) ++mirrored;
SBMirrorLocatorRelease(mirrors);
if (mirrored != 4) return 4;
SBLineRelease(line);
SBParagraphRelease(paragraph);
SBAlgorithmRelease(algorithm);

// Scripts over UTF-8, and Unicode 17.0's Sidetic.
const std::string_view utf8 = "abc \xD8\xB9\xD8\xB1\xD8\xA8\xD9\x8A";
const SBCodepointSequence u8seq{SBStringEncodingUTF8, utf8.data(), utf8.size()};
SBScriptLocatorRef scripts = SBScriptLocatorCreate();
SBScriptLocatorLoadCodepoints(scripts, &u8seq);
std::vector<SBScript> found;
while (SBScriptLocatorMoveNext(scripts)) found.push_back(SBScriptLocatorGetAgent(scripts)->script);
SBScriptLocatorRelease(scripts);
if (found != std::vector<SBScript>{SBScriptLATN, SBScriptARAB}) return 5;
if (SBCodepointGetBidiType(0x10940) != SBBidiTypeR || SBCodepointGetScript(0x10940) != SBScriptSIDT) return 6;

// The mapped macros, at compile time.
static_assert(SBBidiTypeIsStrong(SBBidiTypeAL) && !SBCodepointIsValid(0xD800u) && SBLevelMax == 125);
return 0;
}
20 changes: 20 additions & 0 deletions tests/mwrap/sheenbidi-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
// header's enumerators and macros with the module's entities.
#include <mwrap/sheenbidi/macros.hpp>

import std;
import mcpplibs.mwrap.sheenbidi;

SB_EXTERN_C_BEGIN
SB_PUBLIC int mwrap_sheenbidi_consumer(int value);
SB_EXTERN_C_END
int mwrap_sheenbidi_consumer(int value) { return value * 2; }

int main() {
namespace m = mcpplibs::mwrap::sheenbidi::macros;
static_assert(m::SBBidiTypeR == ::SBBidiTypeR && std::is_same_v<decltype(m::SBBidiTypeR), const SBBidiType>);
if (::SBCodepointGetBidiType(0x05D0) != SBBidiTypeR) return 1;
if (SBBidiTypeIsStrong(SBBidiTypeR) != (m::SBBidiTypeIsStrong)(SBBidiTypeR)) return 2;
if (&::SBAlgorithmCreate != &SBAlgorithmCreate) return 3;
return mwrap_sheenbidi_consumer(21) == 42 ? 0 : 4;
}
Loading