Skip to content

mcpp: six platforms from one manifest — iOS, Android, Web, every distribution format, and the CLI mapping - #6

Open
Sunrisepeak wants to merge 39 commits into
mainfrom
fix/mcpp-2026.9.12.2-followups
Open

Sunrisepeak wants to merge 39 commits into
mainfrom
fix/mcpp-2026.9.12.2-followups

Conversation

@Sunrisepeak

@Sunrisepeak Sunrisepeak commented Sep 12, 2026 •

Copy link
Copy Markdown
Owner

HuxerUI's mcpp build grows from three desktop rows to six — Linux, Windows, macOS, iOS, Android, Web — with every distribution format reachable from one manifest and one build.mcpp, the SDK CLI driving mcpp projects with the same verbs it drives CMake projects with, and CI asserting each row. The CMake build system is untouched: the only deletions are the WiX definition and AppImage helper the mcpp path itself used to carry, now provided by mcpp:plugins.

Closes HuxerUI#130, closes HuxerUI#131.

Floors

  • mcpp 2026.9.13.1 (kind = "app", per-file mcpp::deploy, target-axis requires_abi, per-target [runtime] frameworks, mcpp run --format).
  • mcpp:plugins 0.9.0 (dist-wix, dist-appimage, dist-apple, dist-apk, dist-web), a build-dependency of huxerui-build-rules only.
  • The Android row needs xim:android-platform@36-r2, which is in Sunrisepeak/xim-pkgindex (PR linked below) and not yet upstream — see Not verified.

Commits

Commit What it does
ci(mcpp) 2026.9.12.2 → follow-ups The earlier part of this PR: pinned engine, operator== for the MSVC STL, windows_subsystem, HuxerUI#130, HuxerUI#131, manifest metadata.
refactor(mcpp) plugins huxerui.rules reaches the formats through mcpp:plugins; its own WiX/AppImage code and tests go.
feat(mcpp) six rows The iOS, Android and Web sections of mcpp.toml; resources deployed where each runtime reads them; every format provided on its row with nothing stated; the Java host loads the application library; HUXERUI_SMOKE_EXIT_MS; no -pthread, requires_abi = { exceptions } on the Web only.
feat(cli) huxerui build/run/package/doctor/platform add on an mcpp project map to mcpp build/run/pack --target <triple> [--format]; create --build mcpp --platform narrows platforms.
docs(mcpp) Design §1/§3/§5/§7/§7a/§9, mcpp/README.md, the platform guide, the build guide.
ci(mcpp) rows Linux smoke + AppImage; new web, ios-sim, android jobs.
feat(mcpp) GTK probe + libraries huxerui::rules::linux_gtk(link) replaces the framework's private pkg-config probe, reading a payload table the rule package declares (host-module [xlings.workspace] reaches every build program); huxerui-build-check keeps it identical to the root's. A library's build program deploys no resource package and provides no format (the macOS runtime deploy collision in example 03).
ci(mcpp) fixes xlings use in every job (a restored cache kept 2026.9.10.2 active); the wasm magic read as hex; the Android job bridges xim:android-platform@36-r2 from openxlings/xim-pkgindex#834 until the index carries it.
fix(mcpp) iOS link The iOS rows pin llvm@20.1.7 in every application manifest (22's libc++ headers reference symbols the SDK's libc++.dylib lacks) and the framework's build program links Apple's libclang_rt.iossim.a for @available. The simulator row links and bundles.
refactor(mcpp) packages huxerui.rules is three host-module packages — huxerui-build-rules (options, codegen, resources, configure()), -gtk (the payload table + linux_gtk()), -dist (the five formats + the templates they ship) — re-exported through one import huxerui.rules;. A partition is not built for a host module, so the split is by package.
feat(app) smoke HUXERUI_SMOKE_EXIT_MS is the Runtime's: a timer thread hands the quit to the platform's UI-thread dispatcher (a bare Xvfb never paints a frame, so nothing frame-based fires there), ended through RequestApplicationQuit() — shutdown runs, the exit status is main()'s.
docs(mcpp) Manifest comments cut to the why; the operator== note names microsoft/STL#6294.
refactor(mcpp) one payload table The Linux GTK table lives only in huxerui-build-rules-gtk's manifest (4 direct modules + their 33-entry .pc closure, pinned); the root's duplicate is gone — measured identical build, run and RPATH — and huxerui-build-check refuses a second copy.
feat(mcpp) resources module hrc --module-name writes <namespace>_resources.cppm (export module app.resources; import huxerui; …) beside the header; the rule declares it to mcpp as a generated module interface (provides/imports on the hrc edge — the mechanism hcg's transformed units use), so a module-style application writes import app.resources; and no header. The header stays for CMake.
feat(templates) live2d huxerui create app <name> --build mcpp --template live2d: a Live2D model (Mao, Free Material License) on all six rows through one huxerui.live2d dependency; pins HuxerUI and Lib-Live2D by git revision (mcpp refuses path + git for one package), so the CLI leaves its dependency lines alone; the templates job creates and builds it. No headers: import app.resources;. Verified in a browser from mcpp pack --target wasm32-emscripten --format web.

What to look at hardest

Every format is provided on its row without opt-in. mcpp pack --format apk|app|web|appimage|msi works on a huxerui create --build mcpp project whose build program states only .target; the configure() options (.installer, .appimage, .apple, .android, .web) only change what a format produces. Off its row a format is unknown (mcpp pack --format apk on a Linux desktop build), which is the honest answer. A member that declines the requested format has its reason repeated as a warning — mcpp discards a successful build program's stderr, and that reason used to vanish behind no action claimed --format 'apk'.

Resources. hrc writes the package once; the rule deploys it per file under <target>.resources/ (Linux/Windows), HuxerUI/ (macOS/iOS bundles, with an AppKit fallback to the directory beside the executable), assets/ (APK) and --preload-file (Web). huxerui-source-select predicts the set of files hrc writes so the merge step's declared outputs cannot drift.

Android. kind = "app" is a shared object on this row; HuxerUIActivity reads org.huxerui.app_library from the manifest the rule ships and loads it, then HuxerUIView's loadLibrary("huxerui") is tolerated failing (the framework is inside the application's library). The Java host uses API 36, so the rule package pins xim:android-platform = "36-r2" over dist-apk's 35-r2; mcpp resolves the two by distance and prints a two-versions warning on every Android build — expected until the member's pin moves (§9).

ABI. -pthread is dropped entirely (no TU carries it; the payload glibc has pthread in libc) so no BMI records a thread model. Only requires_abi = { exceptions = true } on cfg(os = "emscripten") remains, and every template writes the matching [target.'cfg(os = "emscripten")'.abi].

CLI mapping. linux → <arch>-linux-gnu/appimage; windows → <arch>-windows-msvc/msi; macos → <arch>-macos/app; web → wasm32-emscripten/web (run packs and serves the directory); android → x86_64-linux-android (emulator) or aarch64-linux-android (device)/apk; ios → aarch64-ios-sim or aarch64-ios/app. CMake-only options (--source, --build-dir, …) are refused by name.

Verified locally (Linux, mcpp 2026.9.13.1, plugins 0.9.0)

  • mcpp test in mcpp/huxerui-source-select (incl. the new resource-output prediction); huxerui-build-check parity with six rows; mcpp build of the root, mcpp/huxerui-tests, examples 01/02/03.
  • Example 01: native build, bin/huxerui_example_import.resources/huxerui/resources.bin present, HUXERUI_SMOKE_EXIT_MS run exits 0 with the marker, mcpp pack --format appimage → 20.7 MB AppImage.
  • Example 01 Web: mcpp pack --target wasm32-emscripten --format web → index.html, .js, .wasm (magic checked), .data.
  • Example 01 Android: mcpp pack --target x86_64-linux-android --format apk → signed APK with lib/x86_64/libhuxerui_example_import.so, assets/huxerui/resources.bin, classes.dex; apksigner verify passes (with the local 36-r2 recipe).
  • huxerui_cli_tests: 45 cases / 452 assertions, including the new mcpp backend cases.

CI, honestly

Green: parity, Linux, Linux/clang, macOS, Web, Windows, generated templates, Android, and — since the llvm@20.1.7 pin — the iOS simulator. What the green does not yet cover, recorded in design §9:

  • macOS / iOS .app are the executable only. mcpp 2026.9.13.1 refuses to stage a Mach-O program in every --mode (it would have to run it to trace its dynamic closure), so nothing mcpp::deploy placed — HuxerUI/ resources — reaches the bundle. The iOS job asserts the bundle and its Info.plist and skips the simulator smoke until the engine stages; the macOS job packs the library.

Resolved along the way, on the ecosystem side: xim:android-platform@36-r2, xim:glew, xim:esbuild are in the published index (openxlings/xim-pkgindex#834); mcpp:plugins 0.9.1 (mcpp-community/mcpp-plugins#21, mcpp-index#407) links an application's res/ as the APK's base resources and pins 36-r2 itself, so every APK now carries a launcher icon (the SDK's Gradle-template icon set unless the application supplies android/res) and the rule package pins nothing of its own.

Not verified

  • macOS and iOS: no Mac here. The .app bundle, the simulator run through simctl-run, and the Objective-C fallback in appkit_adapter.mm are exercised only by CI.
  • Windows MSI through dist-wix: CI only.
  • The android CI job will fail until xim:android-platform@36-r2 is in the upstream index (the fork PR adds it). Locally the recipe was patched into the registry copy to prove the path.
  • mcpp run --format apk on an emulator was not run; CI inspects the archive.

The msi and appimage providers huxerui.rules carried were the same thing
mcpp:plugins 0.8.0 ships as dist-wix and dist-appimage, and 0.9.0 adds
dist-apple's iOS row, dist-apk and dist-web. The rule package now depends
on the collection and translates HuxerUI's option surface into each
member's options; the WiX definition, the AppImage helper, the argv
builders and their tests go with the providers they served.

xim:wix leaves both manifests: a dist member declares the payload it runs
on its own feature axis, and a host module's declaration reaches every
build program it is compiled into, so neither the framework nor an
application names it (mcpp#622 A8).

The CMake installer -- HuxerUIWindowsInstaller.cmake, the bootstrapper
application and its WiX sources -- is untouched.
…he Web

One manifest now serves six rows. The framework's `platforms` lists them;
the iOS, Android and Web sections mirror IOS.cmake, Android.cmake and
Web.cmake as the desktop sections already mirrored theirs, and
huxerui-build-check compares all six pairs. Android is `env = "android"`
on `os = "linux"`, so the GTK sections narrow to the desktop.

The resource package reaches each runtime where it reads it: deployed
per file (mcpp 2026.9.13.1 protocol 11) under `<target>.resources/` on
the desktop rows, `HuxerUI/` in the Apple bundles, `assets/` in the APK,
and preloaded into MEMFS on the Web. hrc:merge declares the paths it
writes, and huxerui-source-select predicts them so a stale declaration is
a test failure rather than a missing file.

Every distribution format is provided on its row with nothing stated:
`mcpp pack --format msi|appimage|app|web|apk` works on a fresh project,
and `configure()`'s options only change what a format produces. When a
member declines the format `mcpp pack` asked for, its reason -- which mcpp
discards from a successful build program -- is repeated as a warning.

The Java host loads the application's library from
`org.huxerui.app_library` before HuxerUIView looks for libhuxerui.so,
which under mcpp is the same file; the manifest the rule ships names it.
The host compiles against API 36, the compileSdk the Gradle build states,
so the rule package pins xim:android-platform 36-r2 over dist-apk's 35.

An application exits on its own when HUXERUI_SMOKE_EXIT_MS is set, which
is what lets CI run each row's program and assert it started.

The exception model is required on the Web only (`requires_abi`, a
target-axis key mcpp 2026.9.13.1 honours); the `-pthread` dialect flag is
gone, since no translation unit carries it and the payload glibc has
pthread in libc.

tools/resource_compiler is 0.3.1 so that mcpp's per-version tool cache
picks up the depfile flag.
`huxerui build|run|package|doctor|platform add` on a project whose root
is an mcpp.toml maps onto `mcpp build|run|pack --target <triple>
[--format <f>]`: linux to <arch>-linux-gnu and appimage, windows to
<arch>-windows-msvc and msi, macos to <arch>-macos and app, web to
wasm32-emscripten and web (run serves the packed directory), android to
x86_64-linux-android or aarch64-linux-android and apk, ios to
aarch64-ios-sim or aarch64-ios and app. `create --build mcpp --platform`
narrows the manifest's `platforms`, and `platform add` extends it, with
web written as mcpp's emscripten. The CMake-only options are refused by
name on an mcpp project rather than ignored.
The design records the six-row table, the resource deployment prefix per
row, the ABI switches (exceptions on the Web, threads nowhere), the
distribution members and their defaults, and the known gaps: store
signing, Kotlin and iOS devices stay with CMake, a universal APK is one
ABI at a time, the min_api_level sweep warning, the cached host tool
hazard, and the doubled xim:android-platform pin. The guides carry the
mcpp command for each platform beside the CMake one, and the smoke
variable.
Pins mcpp 2026.9.13.1. The Linux job asserts the resource package beside
the executable, runs the example under xvfb with HUXERUI_SMOKE_EXIT_MS
and packs the AppImage. New jobs build and pack the Web directory (page,
launcher, wasm magic, preload data), the iOS simulator bundle
(MinimumOSVersion 15.0, the resource index inside, a simctl-run smoke)
and the x86_64 APK (the application library, the resource index, the
dex, the signature, apksigner verify).

.agents/ holds working design notes and mcpp.lock is a resolution record
of exactly-pinned manifests; neither is committed.
A restored cache keeps whichever mcpp was active when it was saved, and
`xlings install mcpp@<version>` adds the version without switching to
it -- the log said so: "installed, but 'mcpp' still resolves to
2026.9.10.2". Every job now runs `xlings use` and asserts the version
it got.
huxerui::rules::linux_gtk(link) runs the pkg-config probe the framework's
build program used to carry. The payload table it reads is declared in
the rule package's manifest: xpkg_dir answers only for payloads the
building package declared, a host module's [xlings.workspace] counts as
that package's own, and the rule package is compiled into every
application's and library's build program -- so a library whose sources
include GTK headers (Lib-Live2D's GL surface) calls the rule instead of
copying 37 entries. huxerui-build-check asserts the rule's table and
the root manifest's stay identical.

A library's build program no longer deploys the builtin resource
package: on the rows where every package's prefix is the same
(`HuxerUI/`, the APK's assets) a library deploying it beside the
application's copy was `runtime deploy collision`, measured on the macOS
row with example 03. A library with resources of its own gets them
compiled for its own header, and says that they do not reach the
application's package under mcpp yet. The application's own accessor
header (`<namespace>_resources.h`) is now on its include path.

A non-inline function pins std::vector<std::string>'s initializer_list
constructor into the rule module's object: GCC 16 emits an instantiation
an imported inline function needs only in a translation unit that
instantiated the class for itself, and a one-line build program does not.
…agic as hex

The pwsh and clang jobs lacked the `xlings use` the others gained; on
both, the restored cache's mcpp 2026.9.10.2 stayed active and compiled
the plugin collection against an API without pack_format. The Web
check compared `od -c` output against a pattern with single spaces,
which od does not print.
Its build program has no launcher to package, so a member it asked only
declined beside the application's own answer -- and, since the rule
repeats a declined format's reason as a warning, said so on every
`mcpp pack`. The design's known gaps record what the six rows measured:
a library's resources stay with the library, the iOS simulator row's
link (libc++'s atomic-wait table and compiler-rt's `@available`
support are both missing from what the engine composes), and dist-apple
placing deployed directories where NSBundle does not look.
The recipe is in Sunrisepeak/xim-pkgindex#1 and not yet in the published
index. The Android job gives the synced index copy the recipe file and
its cache entry, in a step that says it is temporary and what removes it.
@Sunrisepeak Sunrisepeak changed the title mcpp 2026.9.12.2 follow-ups: GUI subsystem key, the STL find workaround, #130, #131, and pack formats mcpp: six platforms from one manifest — iOS, Android, Web, every distribution format, and the CLI mapping Sep 12, 2026
…cted to fail

The Windows job asserted a rendered Package.wxs, the file the rule's own
WiX provider used to write; dist-wix renders its own and the File table
is the check that matters. The iOS simulator job keeps running with
continue-on-error until mcpp's row links a real application (design §9).
…tages Mach-O programs

Measured on the macOS row: `mcpp pack --format app` refuses to stage a
program in every --mode (the engine would have to run it to trace its
dynamic closure, and dyld ignores LD_TRACE_LOADED_OBJECTS), so dist-apple
bundles the bare executable and nothing deployed reaches it.
…builtins

The simulator row failed to link on two things the engine composes: the
llvm 22.1.8 payload's libc++ headers reference runtime symbols
(`__atomic_*_global_table`, `__hash_memory`) that the SDK's libc++.dylib
does not export, and `@available` compiles to `__isPlatformVersionAtLeast`
from compiler-rt's iOS builtins, which nothing links.

Every application manifest -- the examples and the templates -- pins the
iOS rows to llvm@20.1.7, whose headers reference only what Xcode 26's
libc++ has; the pin is the root's because a toolchain is. The framework's
build program locates the builtins archive (the payload first, then
Xcode's through xcrun) and names it on the link line, which reaches every
consumer's link.
huxerui.rules was one 990-line interface holding four things. mcpp compiles
one interface unit per host-module package (a partition is not built --
measured: `huxerui.rules:probe: failed to read compiled module`), so the
split is by package, and `huxerui.rules` re-exports the two it grew:

  huxerui-build-rules-gtk   the Linux GTK payload table (its manifest) and
                            huxerui::rules::linux_gtk(); huxerui-build-check
                            keeps the table identical to the root's
  huxerui-build-rules-dist  the five distribution formats as mcpp:plugins'
                            members, their option types, the manifest and
                            page templates they ship; provide_formats()
  huxerui-build-rules       options, codegen, resources, configure()

A consumer's `import huxerui.rules;` sees the same names as before.

The launcher icon set the two application templates carried is gone rather
than shipped once by the rule: dist-apk 0.9.0 links a res/ directory as an
aapt2 overlay without --auto-add-overlay, which rejects every resource the
base does not define, so no res/ can be supplied through it yet (design §9).
HUXERUI_SMOKE_EXIT_MS is read by the Runtime: armed on the first frame, so
the delay counts from the moment the application is up, scheduled with
RequestFrameAfter on the frame scheduler, and ended with the same
RequestApplicationQuit an application's own Quit() takes -- so a CI smoke
run exercises shutdown too, and the exit status is main()'s. The detached
thread that called _Exit(0) from beside the platform loop is gone.
The manifests' comments say why a key is there in one to three lines; the
operator== note names the MSVC STL bug (microsoft/STL#6294) and when to
default it again.
With the llvm@20.1.7 pin and Apple's builtins the simulator row links and
dist-apple produces the bundle; what mcpp 2026.9.13.1 cannot do yet is
stage a Mach-O program, so the bundle holds the executable alone. The job
asserts the bundle and its Info.plist, says which state the run is in, and
runs the simulator smoke only once the engine stages. No longer
expected-fail.
Armed on the first frame, the timer never armed under a bare Xvfb: the
Linux adapter waits for the paint of one frame before scheduling the next,
and that paint never comes there, so both Linux jobs timed out at the
smoke run. The Runtime now starts a timer thread at construction and,
when it elapses, hands the quit to the platform's UI-thread dispatcher --
the one channel that reaches the loop from elsewhere -- guarded by a flag
the state's destructor clears on that same thread. The exit path is still
RequestApplicationQuit, and the status still main()'s.
…ndency

`huxerui create app <name> --build mcpp --template live2d` renders an
application that loads a Live2D model (Mao, from Live2D's samples, Free
Material License, under resources/raw/) through the generated raw-resource
identifiers and shows it with ModelView -- the ecosystem-library case as a
template: one `huxerui.live2d` line in the manifest, nothing about Cubism
in the application, all six rows.

The template pins HuxerUI and Lib-Live2D by git revision -- Lib-Live2D
pins the framework at the revision its mcpp build was made against, and
mcpp refuses two sources for one package, so the application names the
same one; both become versions once published. The CLI therefore leaves
this template's dependency lines alone instead of rewriting them to a
path, and the templates job creates and builds it beside the others.
The live2d template's model PNG carries the byte pair the token scan looks
for; a file with a NUL byte is a copied asset, not a template.
hrc gains --module-name and writes, beside the header, a module interface
unit exporting the same declarations over `import huxerui;`. The rule
declares that unit to mcpp as a generated module interface -- `provides`
and `imports` on the hrc edge, the mechanism hcg's transformed units
already use -- so an application's module unit names its resources with
`import app.resources;` and opens with no global module fragment. The
live2d template does; the header stays for the CMake build.

hrc is 0.3.2 for mcpp's per-version tool cache.
HuxerUI at the commit whose hrc writes `app.resources`, and Lib-Live2D
at the commit that pins that same HuxerUI -- one HuxerUI for the graph.
A header-style package -- Lib-Live2D's preview, its app.cpp including the
headers -- has <typeinfo> and the scope prelude forced into every one of its
translation units, and a forced include lands before `export module`, which
no compiler accepts: every native row of the preview failed on the
generated unit. The rule now writes the module only for a package with
module interface units, where nothing is forced; the header-style one keeps
the header it already includes.
…ackage

The root manifest carried the same 37 entries the rule package declares.
A host module's [xlings.workspace] provisions the payloads and reaches
every build program the framework's included, so the root copy added
nothing: with it removed the framework builds, the example runs and its
RPATH is byte-identical. huxerui-build-check now refuses a second copy at
the root and requires the four modules the probe asks for in the one that
remains. The table keeps its transitive closure: pkg-config searches the
declared payloads and nothing else, and the exact pins are what make two
machines build one binary.
Sunrisepeak and others added 7 commits September 13, 2026 14:50
The previous pin was rewritten away by an amend on the library's branch;
from here on that branch only grows, so a pin stays fetchable.
openxlings/xim-pkgindex#834 is merged and the published index (f8c6801)
resolves xim:android-platform@36-r2, so the Android job installs it like
any other payload.
The floor moves to 0.9.1 (mcpp-plugins#21, indexed by mcpp-index#407):
dist-apk links an application's `res/` as its base resources instead of
an aapt2 overlay, and pins `xim:android-platform` 36-r2 itself, so the
rule package's own pin -- and the two-versions warning on every Android
build -- go.

With a `res/` that can hold an icon, the rule supplies one: an
application's `android/res` when it has one, else the launcher icon set
the SDK's Gradle template already ships
(`tools/huxerui_cli/templates/platform/android/app/app/src/main/res`),
so the icon lives once in the repository and the templates carry no
copies. The manifest names `@mipmap/ic_launcher` unconditionally, which
lets the `@@ICON@@` rendering step and the rule's own `read_file` go:
dist-apk reads the SDK's template by absolute path and declares it
`rerun_if_changed` itself.

CI: `xlings install -y --use` activates the pinned mcpp in one line;
the version assertion stays.

Measured: example 01 packed for x86_64-linux-android lists
`res/mipmap-*` and `resources.arsc`, `aapt2 dump badging` reports the
icon and `targetSdkVersion:'36'`, and the pack log has no
version-override warning.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants