Skip to content

CMake parity for a framework and its ecosystem: per-row linkage, dependency identity, shared-library staging, multi-source tests, DMG / Setup.exe — each with a CI check #634

Description

@Sunrisepeak

中文摘要

HuxerUI 的 mcpp 构建链在六个平台上已默认对齐 CMake 的链接、加载与资源布局约定(fork PR 见文末)。逐项对照 CMake 链之后,仍需要 mcpp 生态补齐的共 21 条,按归属分为引擎(A)、mcpp:plugins(B)、xim-pkgindex(C)和 mcpp-index(D)四类。每条都写明实测现象、需求原因、建议形状,以及可在 GitHub 托管 runner 上跑的 CI 验收方法;不需要真机、签名证书或 secret。

  • P1(每个应用和生态库都会碰到):
    • A1:按平台切换依赖的 linkage,第一种直觉写法会被静默忽略;
    • A2:依赖名与目标清单身份不一致时,报错是"模块重复提供";
    • A3 / B1:图内构建的共享库没有被 stage(Android 清单仍写 closure = walked),Mach-O 闭包未遍历,所以 macOS 的 .app 带不上 dylib。
  • P2:
    • 测试:A5 多源文件测试程序与标签;
    • payload 与 runner:A7 payload 依赖闭包对 build program 不可见,C2 模拟器控制台输出偶发丢失,C4 未合并 recipe 无法在消费方 CI 中测试;
    • macOS 分发:B2 DMG,B3 / C1 mcpp run --format app 拿不到程序退出码。
  • P3:
    • 引擎:A4 默认 SONAME,A6 模拟器/模拟机上跑测试,A8 build program 枚举依赖图与元数据,A9 依赖声明平台下限,A10 --toolchain
    • 插件与 payload:B4 / C3 Setup.exe(Burn),B5 AAB,B6 metal / esbuild 规则;
    • 索引:D1 发布六平台版本并改用版本范围。

Context

#622 asked for what a UI framework needs on Android, iOS and the Web; #630 listed what the first six-platform build still hit. Both are answered: 2026.9.13.2, 2026.9.14.1 (#633) and mcpp:plugins 0.9.3 are what HuxerUI now builds on. On every row its mcpp path takes the CMake path's conventions by default:

  • the framework is statically linked, except on Android, where it is its own libhuxerui.so loaded by the Java host;
  • resources land where the CMake build puts them;
  • CMake's own windowless UI smoke runs under mcpp test.

The work is on three fork PRs, each green on every row: Sunrisepeak/HuxerUI#7 (framework, rule packages, CLI), Sunrisepeak/Lib-Live2D#2 (an ecosystem library) and Sunrisepeak/HuxerUI#8 (templates).

This issue is what remains when that mcpp path is compared with the CMake path item by item: linking, loading, packaging, tests and distributables. Each item gives:

  • Measured: the fact, with the version it was measured on;
  • Why: why it matters beyond HuxerUI;
  • Proposed: a shape;
  • CI check: a check that runs on a GitHub-hosted runner.

Android checks use the emulator (KVM on ubuntu-24.04) and iOS checks the simulator (macos-15). No check needs a device, a signing identity or a secret.

Measured with mcpp 2026.9.14.1, mcpp:plugins 0.9.3 and xlings 2026.9.12.1, on a Linux x86_64 host (GCC 16.1.0, xim:android-ndk 30.0.16248370) and in CI, unless an item says otherwise. CMake references point at unchanged upstream files, through the fork at 64264cb.

The only open item of #622 (B4) is carried here as B6, so #622 can be closed in favour of this issue.

# Where What Priority
A1 engine One identity, a different linkage per row: the first spelling is dropped silently P1
A2 engine A dependency key that names a different identity than its manifest: two packages from one directory P1
A3 engine Shared libraries the graph built are not staged; Mach-O closures are not walked P1
A4 engine No SONAME / install name for a dependency made shared by linkage P3
A5 engine A test program from several sources; test roots; labels P2
A6 engine Windowless tests on the Android emulator and the iOS simulator P3
A7 engine A payload's recipe-dependency closure is invisible to build programs P2
A8 engine Enumerating the resolved graph, and package metadata, from a build program P3
A9 engine A platform floor a dependency needs (min_api_level, iOS deployment target) P3
A10 engine --toolchain on run / test / pack P3
B1 plugins dist-apple carries graph-built dylibs (the plugin-side A3, as dist-apk 0.9.3 did) P1 if before A3
B2 plugins dist-apple: DMG P2
B3 plugins dist-apple: a default macOS --format app runner that returns the program's status P2
B4 plugins dist-wix: a Burn bundle (Setup.exe) P3
B5 plugins dist-apk: an Android App Bundle P3
B6 plugins rules-metal, tools-esbuild (carried from #622 B4) P3
C1 xim-pkgindex A macOS app-bundle runner program P2
C2 xim-pkgindex simctl-run: output lost under --console-pty; exit status unmeasured P2
C3 xim-pkgindex WiX Burn native SDK payloads P3
C4 xim-pkgindex / xlings Resolving an unmerged recipe from a consumer's CI P2
D1 mcpp-index Six-row huxerui.huxerui and huxerui.live2d, so libraries declare ranges P3

A. Engine (mcpp)

A1. One identity, a different linkage per row — P1

Measured. HuxerUI's framework must be a shared library on Android and static elsewhere. The Java host loads libhuxerui.so by name, and CMake's _huxerui_select_framework_target refuses a static framework on Android (cmake/HuxerUILibraries.cmake#L14-L28). The application manifest can say this three ways:

Application manifest Result on x86_64-linux-android
[dependencies] huxerui.huxerui = { path = "../../.." } and [target.'cfg(env = "android")'.dependencies] huxerui.huxerui = { path = "../../..", linkage = "shared" } Builds, and the Android linkage is dropped silently. There is no libhuxerui.so; the application's .so has no NEEDED libhuxerui.so and defines all 54 Java_org_huxerui_* symbols itself. No diagnostic.
the same [dependencies] line and [target.'cfg(env = "android")'.dependencies] huxerui.huxerui = { linkage = "shared" } Refused, with a message about something else (below).
no [dependencies] line; cfg(env = "android") with linkage = "shared" plus cfg(not(env = "android")) without it Works: bin/libhuxerui.so, the application has NEEDED libhuxerui.so and RUNPATH $ORIGIN. This is what HuxerUI ships.

The second spelling is rejected like this:

warning: [dependencies] huxerui.huxerui.linkage = 'shared' is not a version RANGE (version: not a number ('shared')), so it is used as an exact index key: …
error: dependency 'huxerui.huxerui.linkage': no package found for exact selector
  tried: (huxerui.huxerui, linkage)

Why.

  • The spelling that works needs two complementary predicates for one dependency, in every application and every template.
  • The spelling people try first succeeds, and it produces the wrong artefact on exactly the row where linkage matters. An APK built from it carries no libhuxerui.so for the Java host's System.loadLibrary("huxerui").
  • Any library with a JNI or dlopen contract on one platform is in the same position.

Proposed.

  1. Merge conditional linkage. When a conditional declaration names an identity that is also declared unconditionally with the same reference, merge them: the conditional linkage applies on the rows its predicate selects. If merging is unwanted, refuse and name both tables. Never drop it silently.
  2. Accept a modifier-only table. In a conditional dependency table, read { linkage = "shared" } (no version, path or git) as "the declaration above, with this linkage on these rows". If that shape stays refused, say so, instead of parsing linkage as a package name.
  3. Let a dependency state the need. Follow the requires_abi shape (docs/22): [target.'cfg(env = "android")'] requires_linkage = "shared". The root still decides. A build whose resolved linkage differs is refused before compiling, naming the package and the selector.

CI check (ubuntu-24.04, xim:android-ndk).

  • Fixture: an application plus one lib dependency.
  • Spellings 1 and 2: after mcpp build --target x86_64-linux-android, bin/lib<dep>.so exists and llvm-readelf -d of the application's .so shows NEEDED lib<dep>.so. The host build has no lib<dep>.so.
  • Proposal 3: the dependency states requires_linkage and the root does not; the build exits non-zero with the named refusal.

A2. A dependency key that names a different identity than its manifest — P1

Measured. In mcpp/examples/03-library, both edges point at the same directory, whose manifest declares namespace = "huxerui", name = "huxerui":

  • the application declares huxerui.huxerui = { path = "../../.." } (per row, as in A1);
  • its component library declared huxerui = { path = "../../../.." }, which normalises to (mcpplibs, huxerui) (docs/05).

The graph therefore holds two packages built from one directory, and the scanner fails:

error: scanner errors:
  …/HuxerUI/modules/huxerui.cppm: module 'huxerui' already provided by …/HuxerUI/modules/huxerui.cppm

This happened on the macOS and Windows rows in CI (run 34772646907) and reproduces on Linux. The message names neither dependency edge; the fix (spell the library's key huxerui.huxerui) was found by reading every manifest in the graph.

Why. Every ecosystem library declares the framework it builds on. One key written without the namespace, in any library of the graph, breaks every application that also names the framework, and the diagnostic names a module rather than a manifest line.

Proposed.

  • Check the key against the manifest. Read a path or git dependency's identity from the manifest it resolves to. A key that normalises to a different identity is reported, for example as dependency/identity-mismatch: the key, what it normalises to, and what the manifest declares. Then either refuse, or take the manifest's identity; pick one rule and state it.
  • Report duplicates before scanning. Two identities that resolve to one canonical directory, or to one git URL and commit, are reported before scanning, naming both requesters.

CI check (ubuntu-24.04).

  • Mismatch fixture: the application declares ns.dep = { path }; its library declares dep = { path } for the same directory, whose manifest is ns.dep. The build shows the named diagnostic, not a scanner error.
  • Matching keys: the same fixture builds.

A3. Shared libraries the graph built are not staged; Mach-O closures are not walked — P1

Measured.

What the CMake path does. _huxerui_deploy_runtime (cmake/HuxerUIRuntimeDependencies.cmake#L640) runs at install time:

  1. file(GET_RUNTIME_DEPENDENCIES) finds the dependencies.
  2. Dylibs outside /System/Library/ and /usr/lib/ are copied into Contents/Frameworks.
  3. install_name_tool rewrites names to @rpath/…, deletes the old rpaths, and adds @executable_path/../Frameworks to the executable and @loader_path/… to libraries.
  4. codesign --force --sign - runs on each file and on the bundle.
  5. codesign --verify --deep --strict checks the result.

The same function copies DLLs beside the executable on Windows and fills lib/ with patchelf on Linux.

Proposed.

  1. Stage what the graph built. Stage every shared library the graph built for the artefact, on every row, however the closure is discovered: those libraries are declared, not discovered.
  2. Walk with the static readers. Use needed_names (ELF, PE and Mach-O) instead of running the program. Cross rows then get a real walk, and closure = walked means complete.
  3. Mach-O specifics.
    • Shared targets get the install name @rpath/lib<name>.dylib by default.
    • A program packed into a bundle gets LC_RPATH @executable_path/../Frameworks. Setting this at link time, as the §4 note suggests, may make a load-command editor unnecessary.
    • Any rewrite of a Mach-O file is followed by an ad-hoc re-sign, because arm64 refuses an invalidated signature.

CI check.

  • ubuntu-24.04: pack A1's fixture for x86_64-linux-android; the stage manifest lists lib/lib<dep>.so.
  • macos-15: an application with a linkage = "shared" dependency, packed with mcpp pack --format app:
    • Contents/Frameworks/lib<dep>.dylib exists;
    • otool -L Contents/MacOS/<exe> names @rpath/lib<dep>.dylib, and otool -l shows LC_RPATH @executable_path/../Frameworks;
    • codesign --verify --deep --strict passes, and Contents/MacOS/<exe> exits 0.

A4. No SONAME / install name for a dependency made shared by linkage — P3

Measured. libhuxerui.so, produced by linkage = "shared" for x86_64-linux-android, has no DT_SONAME. The application records NEEDED libhuxerui.so from the file name, plus RUNPATH $ORIGIN. It loads on Android. [targets.<n>] soname exists, but a dependency made shared by its consumer's linkage has nowhere to state one.

Proposed. A target resolved Shared through linkage defaults to the SONAME lib<target>.so, and to the Mach-O install name from A3. [targets.<n>] soname still overrides it.

CI check.

  • ubuntu-24.04: llvm-readelf -d bin/lib<dep>.so shows SONAME lib<dep>.so.
  • macos-15: otool -D prints @rpath/lib<dep>.dylib.

A5. A test program from several sources; test roots; labels — P2

Measured.

  • mcpp's model. In docs/08, a test is tests/**/*.cpp, and one .cpp makes one program. [build] sources link into every test program.

  • Why HuxerUI needs a separate package. Its tests live in mcpp/huxerui-tests, because the repository's tests/ belongs to CMake: 111 files, some driven by CMake or Python, some linking the CLI, some per platform.

  • CMake's shape. CMake builds one executable per suite from a list of sources, with labels:

    • HuxerUIUnitTests (common unit, 19 files);
    • HuxerUIRuntimeTests (common runtime, 41 files);
    • HuxerUIUiTests and HuxerUILibraryTests;
    • per-platform suites labelled platform plus native or portable.

    The release workflow selects them with ctest -LE platform, -L portable and -L native.

  • Measured today. The 41 runtime files compile and pass unmodified in the mcpp package:

    All tests passed (41014 assertions in 1091 test cases)
    test result ok. 2 passed; 0 failed; finished in 82.14s (build 72.52s + run 3.38s)
    

    They only fit by being added to [build] sources, which puts them into the same program as the unit suite, and into the smoke program too.

Why.

  • One process per suite, as CMake has, so a suite's global state cannot leak into another suite.
  • Selection by label in CI.
  • Not linking every test source into every test program.
  • Any project whose tests/ directory already belongs to another build system has the same problem.

Proposed.

[test]
roots = ["tests-mcpp"]          # or: discover = false

[[test]]                        # a named program built from several sources
name    = "runtime"
sources = ["../../tests/runtime/*.cpp", "../../tests/support/builtin_resources.cpp"]
defines = [""]
labels  = ["common", "runtime"]
timeout = 120

Add mcpp test --label <l> and --exclude-label <l>, and put labels into --message-format json.

CI check (ubuntu-24.04). A fixture with two [[test]] programs whose sources do not overlap:

  • each defines a marker symbol, and nm finds each marker only in its own binary;
  • mcpp test --label a runs one program;
  • the JSON records carry the labels.

A6. Windowless tests on the Android emulator and the iOS simulator — P3, verification first

Measured. Both runners already accept a bare executable:

  • adb-run pushes it to /data/local/tmp, runs it with one adb shell, and parses the status back;
  • simctl-run runs it with simctl spawn.

mcpp test --target x86_64-linux-android and --target aarch64-ios-sim should therefore run windowless test programs, but this is unmeasured. The likely gap is the files a test reads: HuxerUI's tests compile a resource package's host-absolute path in (HUXERUI_TEST_BUILTIN_RESOURCE_PACKAGE), and no runner transfers it. CMake has platform-specific carriers for the smoke (an instrumentation .so on Android, an XCTest bundle on iOS). mcpp could cover more of the suite with bare executables.

Proposed. Measure it and document the result. If files are the gap, let a test's deployed files (mcpp::deploy) travel with it, found through a relative path (the working directory, or MCPP_TEST_DATA_DIR).

CI check. A fixture test reads a deployed file and exits 0 or 1 on its content:

  • ubuntu-24.04: on an x86_64 API 34 emulator (KVM), mcpp test --target x86_64-linux-android passes.
  • macos-15: mcpp test --target aarch64-ios-sim passes.

A7. A payload's recipe-dependency closure is invisible to build programs — P2

Measured.

  • What HuxerUI asks for. Its Linux build asks pkg-config for gtk4, epoxy, gio-2.0 and libsoup-3.0.

  • What happens with only those declared. Declaring just the four direct payloads (xim:gtk4, xim:libepoxy, xim:libsoup, xim:glib) is not enough. The 33 payloads that make up their .pc closure are invisible to the build program, even though they are installed on the machine (xpkg_dir answers ""), and pkg-config fails:

    Package 'pango', required by 'gtk4', not found
    Package 'pangocairo', required by 'gtk4', not found
    Package 'gdk-pixbuf-2.0', required by 'gtk4', not found
    Package 'cairo', required by 'gtk4', not found
    
  • The workaround. HuxerUI declares and exactly pins all 37 payloads in mcpp/huxerui-build-rules-gtk/mcpp.toml, and updates that closure by hand whenever a recipe changes.

Why. Any package that consumes a pkg-config stack through xim payloads (GTK, GStreamer, FFmpeg) has to copy its closure into its own manifest.

Proposed.

  • Answer for the closure. xpkg_dir answers for the recipe-dependency closure of a declared payload, at the versions actually installed. Those versions are recorded in mcpp.lock, so two machines still build the same binary.
  • Expose pkg-config directories. The build program receives the closure's lib/pkgconfig and share/pkgconfig directories, as mcpp::pkg_config_path() or MCPP_PKG_CONFIG_PATH.
  • Related: feat/RFC: [system_deps] —— 经 pkg-config 集成宿主系统库 #493 is the host-system counterpart.

CI check (ubuntu-24.04). A fixture declares only xim:gtk4:

  • its build program runs pkg-config --cflags --libs gtk4 against the provided path and exits 0;
  • xpkg_dir("xim", "pango") is non-empty;
  • mcpp.lock records pango's version.

A8. Enumerating the resolved graph, and package metadata, from a build program — P3

Measured.

  • mcpp::dep_dir / MCPP_DEP_<NAME>_DIR answer by name, for each declared dependency.
  • A build program cannot enumerate the resolved graph, transitive dependencies included, and cannot read anything a dependency states about itself.
  • CMake's huxerui_use_library merges each HuxerUI library's resource package into the application's. On the mcpp path, an application cannot find which of its transitive dependencies carry resources, so a library's resources stay unmerged. Lib-Live2D's string catalogue is today's case.

Proposed.

  • Enumerate the graph. mcpp::dependencies() returns, for each package of the resolved graph: identity, version, kind, resolved linkage, manifest directory and requesters.
  • Read package metadata. A [package.metadata.<tool>] table is readable from consumers' build programs (for example mcpp::dep_metadata(identity, "<tool>.<key>")), and editing it re-runs those build programs.

CI check (ubuntu-24.04). Fixture: application → lib A → lib B, where B declares [package.metadata.demo] resources = "res".

  • The application's build program prints the graph and B's value, and the check asserts both.
  • Editing B's metadata re-runs the application's build program.

A9. A platform floor a dependency needs — P3

Measured. min_api_level ([target.<triple>]) and ios_deployment_target ([build]) belong to the artefact, so only the root states them. HuxerUI's manifests state API 23 and iOS 15.0. Every application and template restates min_api_level = 23 on both Android rows and ios_deployment_target = "15.0", because a dependency has no way to say what it needs.

Proposed. Follow the requires_abi shape:

[target.'cfg(env = "android")']
requires_platform = { api_level = 23 }

[target.'cfg(os = "ios")']
requires_platform = { deployment_target = "15.0" }

A root whose resolved floor is lower is refused before compiling, naming the package. An unset iOS floor resolves to the SDK version (#633) and is compared as that.

CI check.

  • ubuntu-24.04: the dependency requires 23 and the root states 21, so the build is refused by name. With the root at 24, the output shows Target x86_64-linux-android → x86_64-unknown-linux-android24.
  • macos-15: the same for aarch64-ios-sim.

A10. --toolchain on run / test / pack — P3

Measured. mcpp build accepts --toolchain; mcpp run, mcpp test and mcpp pack answer error: unknown option: --toolchain. HuxerUI's clang CI leg therefore switches the whole job with mcpp toolchain default llvm@22.1.8.

Proposed. Accept --toolchain on run, test and pack, meaning what it means for build.

CI check (ubuntu-24.04). mcpp test --toolchain llvm@22.1.8 and mcpp pack --toolchain llvm@22.1.8 compile with clang, as mcpp why toolchain or the compile lines show, while the default toolchain is gcc.


B. mcpp:plugins

B1. dist-apple carries graph-built dylibs — P1 if it lands before A3

The plugin-side counterpart of A3, as dist-apk 0.9.3 did for Android:

  1. Read LC_LOAD_DYLIB with llvm-otool -L or llvm-objdump --macho --dylibs-used.
  2. Copy the dylibs the graph built from bin/ into Contents/Frameworks/ (on iOS, Frameworks/).
  3. Add @executable_path/../Frameworks (on iOS, @executable_path/Frameworks) with llvm-install-name-tool.
  4. Re-sign ad hoc, because arm64 refuses an invalidated signature.

CI check. The macos-15 check of A3, and the same for aarch64-ios-sim with simctl-run launching the bundle.

B2. dist-apple: DMG — P2

What the CMake path does. huxerui package macos (tools/huxerui_cli/platform/macos.cpp#L86-L101):

  1. installs the component into a staging directory;
  2. adds an Applications → /Applications symlink beside the bundle;
  3. runs hdiutil create -volname <name> -srcfolder <staging> -format UDZO -ov <target>-<version>.dmg.

On the mcpp path dist-apple stops at the .app.

Proposed. mcpp pack --format dmg on the macOS row: the .app the member already produces, plus the symlink, through hdiutil from the base macOS install (as ditto and codesign are today). Options for the volume name, and later a background image.

CI check (macos-15).

  • hdiutil verify <dmg> passes.
  • After hdiutil attach -nobrowse -readonly -mountpoint "$RUNNER_TEMP/m" <dmg>, <Name>.app/Contents/MacOS/<exe> exists and Applications is a symlink to /Applications.
  • hdiutil detach succeeds.

B3. dist-apple: a default macOS --format app runner that returns the program's status — P2

Measured.

  • Why HuxerUI declares a runner. It resolves resources through NSBundle, so mcpp run --format app must run the bundle; HuxerUI's manifests declare [target.aarch64-macos] runner = ["open", "-W"].
  • Confirmed in CI. mcpp hands the .app path to that argv: a stub open placed first on PATH received -W and /Users/runner/…/huxerui_example_import.app (run 34775426211, step "Hand the bundle to the manifest's runner").
  • The problem. open returns its own status, not the program's, and the program's stdout and stderr do not reach the terminal. mcpp run --format app cannot fail when the application does.

Proposed. dist-apple supplies the default runner for --format app on *-macos through mcpp::runner, which reaches the consumer (docs/41). It names a program (C1) that runs the bundle's CFBundleExecutable in the foreground, with stdio attached, and returns its status. Applications then drop the table; an override stays possible.

CI check (macos-15). A fixture .app whose program prints 1-2-3 and exits 7, with no runner in its manifest: mcpp run --format app prints 1-2-3 and exits 7.

B4. dist-wix: a Burn bundle (Setup.exe) — P3

What the CMake path does. huxerui_add_windows_installer() (cmake/HuxerUIWindowsInstaller.cmake#L116):

  • builds platform/windows/windows_installer.cpp, a WiX Burn bootstrapper application whose interface is written with HuxerUI;
  • links it against the WiX native SDK, which it restores from NuGet with pinned sha256 (C3);
  • runs wix build on a bundle that chains the MSI, producing <target>-Setup-<version>.exe.

wix.exe needs .NET; CMake probes for Microsoft.NETCore.App 6.0 or newer.

Measured on the mcpp side. dist-wix produces the MSI. xim:wix 5.0.2 already carries x64/burn.exe beside wix.exe (dist/wix.cppm, wix_payload_exe).

Proposed. mcpp pack --format setup (or bundle) builds a Burn bundle that chains the MSI dist-wix builds:

  • by default with the standard bootstrapper (WixToolset.BootstrapperApplications.wixext);
  • optionally with a custom bootstrapper program the package itself builds (options.bootstrapper = "<target>", reached as ${mcpp.target_file:<target>}).

CI check (windows-2022).

  • Setup.exe /quiet /norestart /log setup.log exits 0, and the installed executable is present.
  • Setup.exe /uninstall /quiet exits 0, and the executable is removed.
  • The same passes with a minimal custom bootstrapper fixture (C3).

B5. dist-apk: an Android App Bundle — P3

Measured. dist-apk builds and signs an APK: the published debug key by default, or a release keystore given as a package with an alias and a password environment variable. Google Play takes an Android App Bundle for new applications. An application's Gradle shell produces one with bundleRelease; mcpp pack has no aab.

Proposed. mcpp pack --format aab:

  • aapt2 link --proto-format builds the resources;
  • bundletool build-bundle packs the module zip (manifest/, dex/, lib/<abi>/, assets/, res/, resources.pb);
  • jarsigner signs it with the keystore options the APK already takes;
  • several --target values make one bundle, as they make one universal APK today.

This needs a bundletool payload; it is a jar, run by the xim:jdk-* payload the member already uses.

CI check (ubuntu-24.04).

  • bundletool validate --bundle app.aab passes.
  • bundletool build-apks --mode=universal produces universal.apk, which contains lib/x86_64/lib<app>.so.
  • bundletool dump manifest shows the application id.

B6. rules-metal, tools-esbuild (carried from #622 B4) — P3

Measured. Lib-Live2D's mcpp/cubism package writes its Metal shader edges by hand: .metal.air.metallib through xcrun, 477 outputs checked in its CI. Its build program also bundles the TypeScript Web bridge with esbuild from a payload. Both are the same kind of rule as rules-spirv.

Proposed.

  • rules-metal: actions with depfiles, choosing the macOS, iOS or iOS-simulator SDK from the target.
  • tools-esbuild: one action with glob inputs and a declared output.

CI check.

  • macos-15: two .metal files produce one .metallib each for aarch64-macos and for aarch64-ios-sim; touching one .metal rebuilds only its output.
  • ubuntu-24.04: a TypeScript entry produces a bundled .js that exports the entry's symbol.

C. xim-pkgindex (payloads and runner programs)

C1. A macOS app-bundle runner program — P2

For B3: macapp-run <App.app> [args…].

  • It reads CFBundleExecutable from Contents/Info.plist.
  • It runs <App.app>/Contents/MacOS/<exe> in the foreground with stdio attached; NSBundle still resolves the bundle.
  • It returns the program's status.

It lives in apple-simulator-tools or in a new apple-app-tools.

CI check (macos-15). The B3 check, plus a missing or non-bundle operand producing a named error and exit code 2.

C2. simctl-run: output lost under --console-pty; exit status unmeasured — P2

Measured.

Proposed.

  • Measure --console (pipes) and --stdout=<file> --stderr=<file> followed by waiting for the process to exit, against --console-pty.
  • Keep the one that never loses output, print the output, and return the application's own status.
  • Record the measurement in the recipe.

CI check (macos-15).

  • The plugins' iOS fixture runs 20 times in a matrix, and every run prints 1-2-3.
  • A fixture that exits 7 makes mcpp run --target aarch64-ios-sim --format app exit 7.

C3. WiX Burn native SDK payloads — P3

For B4 with a custom bootstrapper, two packages as HuxerUI's CMake path restores them (cmake/HuxerUIWindowsInstaller.cmake#L40-L112):

  • WixToolset.BootstrapperApplicationApi: BootstrapperApplication.h, build/native/v14/x64/balutil.lib, runtimes/win-x64/native/mbanative.dll;
  • WixToolset.DUtil: dutil.h, build/native/v14/x64/dutil.lib.

Also state whether xim:wix carries the .NET runtime wix.exe needs, or which payload does.

Open question for the check. Can mcpp's Windows toolchain (clang, lld-link) link the MSVC-built v14 static libraries? If not, windows = "msvc@system" is the path (#604 is closed).

CI check (windows-2022). A minimal bootstrapper program that calls one balutil and one dutil function compiles and links under mcpp with the payloads, and B4's bundle builds with it.

C4. Resolving an unmerged recipe from a consumer's CI — P2

Measured.

  • The documented limitation. docs/32 says: "A payload's own CI cannot verify that a consumer resolves it".
  • The workaround used. HuxerUI's and Lib-Live2D's CI needed recipes that were still in review (android-platform 36-r2, glew, esbuild). They bridged them by copying the .lua into ~/.mcpp/registry/data/xim-pkgindex/pkgs/<x>/ and adding an entry to .xlings-index-cache.json, which mcpp index update wipes.
  • --add-xpkg did not help. xlings config --add-xpkg registered the recipe under local:, which does not satisfy an xim: address (measured on Sept 12). xlings 2026.9.12.1 also has config --index-repo <NS:URL>, and MCPP_HOME keeps xim-index-repos/ and xim-pkgindex-local/; whether any of them can serve an xim: address from a PR branch was not evaluated.

Proposed. One documented and CI-tested way for an mcpp build to resolve xim: recipes from an unmerged xim-pkgindex branch:

  • either an index overlay under MCPP_HOME, or xlings' --index-repo honoured by mcpp;
  • it survives mcpp index update;
  • mcpp why deps reports it as coming from the overlay.

CI check (xim-pkgindex PR workflow, ubuntu-24.04).

  • A recipe added on the PR branch resolves through the overlay, and a consumer fixture that declares it builds.
  • mcpp index update followed by mcpp build still resolves it.
  • Without the overlay, the build refuses and names the index revision.

D. mcpp-index

D1. Six-row huxerui.huxerui and huxerui.live2d, so libraries declare ranges — P3, after the upstream release

State.

Proposed. Once HuxerUI releases its six-row manifest:

  • publish the next huxerui.huxerui with six platforms, and publish huxerui.live2d;
  • libraries declare huxerui.huxerui = "^0.3", and templates declare published versions.

CI check (mcpp-index PR workflow).

  • mcpp xpkg parse passes on both descriptors.
  • A consumer fixture has huxerui.live2d's ^0.3 requirement and an application pin of 0.3.x. It builds for the host, for x86_64-linux-android and for wasm32-emscripten, and mcpp.lock records exactly one huxerui.huxerui.

E. Not asked of mcpp (HuxerUI's side, recorded for the boundary)

  • Android static framework. HuxerUI needs a Java-side switch, because HuxerUIView's static initialiser loads libhuxerui.so unconditionally. Measured: with the framework static, all 54 Java_org_huxerui_* JNI functions are defined and exported by the application's .so. Beyond A1, nothing is needed from the engine.
  • Runner tables. huxerui.rules can supply the Android and iOS-simulator runners through mcpp::runner (docs/41), which removes the [target.*] runner tables from application manifests. macOS waits for B3 and C1.
  • Test suites.
    • tests/runtime moves into mcpp/huxerui-tests; measured to pass, and cleaner with A5.
    • tests/runtime/ui_testing.cpp needs a resource-compiler fixture.
    • tests/runtime/libraries.cpp needs tests/libraries/root_service as an mcpp package.
  • Leftover workaround. The c++23 workaround in HuxerUI's examples was for clang on Windows hardcodes importStdMinLevel = 23 instead of probing the MSVC STL #603, which is closed, and can be removed.
  • Installer UI. The installer interface program for B4 is HuxerUI's own to build.

F. CI checks at a glance

Item Runner Fixture Passes when
A1 ubuntu-24.04 + android-ndk app + lib, three spellings NEEDED lib<dep>.so on Android for spellings 1 and 2; named refusal for requires_linkage
A2 ubuntu-24.04 app + lib, mismatched keys for one directory named identity diagnostic before scanning
A3 ubuntu-24.04, macos-15 app + shared lib stage manifest lists lib<dep>.so; .app has Frameworks/lib<dep>.dylib, rpath, valid signature, runs
A4 ubuntu-24.04, macos-15 shared lib via linkage SONAME / @rpath install name
A5 ubuntu-24.04 two [[test]] programs disjoint symbols; --label selects; labels in JSON
A6 ubuntu-24.04 (emulator), macos-15 (simulator) test reading a deployed file passes on both
A7 ubuntu-24.04 only xim:gtk4 declared pkg-config gtk4 succeeds; lock lists the closure
A8 ubuntu-24.04 app → A → B with metadata graph and metadata printed; metadata edit re-runs
A9 ubuntu-24.04, macos-15 lib with requires_platform refusal below the floor; triple at or above it
A10 ubuntu-24.04 any package test and pack honour --toolchain
B1 macos-15 as A3 as A3, plus iOS simulator launch
B2 macos-15 any app hdiutil verify; mounted layout
B3 + C1 macos-15 app exits 7, prints 1-2-3 mcpp run --format app exits 7 and prints
B4 + C3 windows-2022 app + MSI (+ minimal bootstrapper) silent install and uninstall
B5 ubuntu-24.04 any app bundletool validate; universal APK contents
B6 macos-15, ubuntu-24.04 .metal sources; TypeScript entry outputs exist; incremental rebuild
C2 macos-15 plugins' iOS fixture ×20 output every time; exit status propagated
C4 ubuntu-24.04 recipe on a PR branch resolves through the overlay, survives index update
D1 ubuntu-24.04 huxerui.live2d on ^0.3 one huxerui.huxerui in mcpp.lock on three rows

G. Suggested order

  1. A1, A2. Every application and ecosystem library hits them. The engine changes are small, and the fixtures run on ubuntu.
  2. A3, or B1 first. The staged tree becomes truthful on Android, and macOS gains a shared mode.
  3. B3 + C1, C2. mcpp run reports the program's own status on macOS and on the iOS simulator, which makes CI results trustworthy.
  4. A5, A7, C4. Test parity with CMake, the 37-line GTK table, and testing recipe PRs.
  5. B2, then C3 + B4, then B5. The distributables the CMake path still owns alone.
  6. A4, A6, A8, A9, A10, B6, D1.

H. Evidence

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions