You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Packaging parity measured by a framework's build-systems conformance check: strip graph-built libraries (E5), feature tools that depend on their SDK (E6), git sub-packages (E7), dependency features on the command line (E8), a pack report (E9), the llvm row's MSVC runtime (E10) #649
HuxerUI builds with CMake and with mcpp. Its specification (docs/design/build-systems-spec.md on Sunrisepeak/HuxerUI#7) states that a project built with either build system's defaults is the same program. build-systems-conformance.yml checks that on every platform:
The same application is created for each build system.
Both are packaged with huxerui package <platform>.
Facts are read from the two artifacts and compared: the manifest and Info.plist entries, icons, resources with their digests, the libraries and how they are stored, the Burn bundle and MSI contents, and so on.
dist-apk strips native libraries and stores them in place when the manifest asks;
dist-appimage takes an SVG icon.
The engine: the items below. All six platforms now agree at ed896c9. Some of the fixes are workarounds this issue would let the plugins and the framework drop.
Measured with mcpp 2026.9.15.2 and mcpp:plugins 0.11.1, on Linux x86_64 unless stated. Source references are to main at 2fc7b5b. The fixtures are throwaway directories, every file is quoted, and the home directory is written as ~.
#
What
Kind
Needed by
E5
mcpp pack strips the program on every row and the shared libraries the graph builds
defect
APK size parity with Gradle; --no-strip / --debug-symbols reaching every packed library
E6
A feature-gated tool may depend on the package that declares it
defect
an SDK shipping a program built against itself
E7
A git dependency can name another package in the repository
enhancement
multi-package repositories pinned by git
E8
--features <dependency>/<feature>; an unknown name is reported with no [features] table
enhancement + defect
a driver opening a dependency's packaging feature for one command
E9
A machine-readable report from mcpp pack; --release / --dev on pack
enhancement
tools that publish what mcpp pack produced
E10
The C/C++ runtime model of the llvm row on x86_64-windows-msvc
question
runtime parity with MSVC builds
E5. mcpp pack leaves an Android row's program, and every graph-built shared library, unstripped
Fixture. An application with a shared dependency, packed for Android and, with a bin target, for the Linux desktop.
lib/libdep.so is not stripped: it keeps .symtab and 7 .debug_* sections (16 592 bytes).
In an application. HuxerUI's two-ABI APK, arm64-v8a, before dist-apk 0.11.1 stripped (bytes):
library
mcpp
Gradle
libconformance.so (the program)
123 272
31 512
libhuxerui.so (the framework, a shared library the graph builds)
8 107 904
5 465 256
libc++_shared.so (the NDK's)
9 485 032
1 374 336
The Android Gradle plugin strips every native library it packages with llvm-strip --strip-unneeded, the NDK's libc++_shared.so included.
Cause, partly.strip_program in src/pack/pack.cppm strips the staged program with the Executable shape and nothing else. Its comment gives the reason:
A bundled .so is somebody else's file: it came out of the store or off the host, mcpp did not build it, and stripping it would change a shared payload's bytes for no gain to this bundle.
That reason holds for store and host files. It does not hold for a shared library this graph compiled from source (libdep.so above). On the Android row the program itself is not stripped either, although the line says stripped; that path was not traced.
Why it matters beyond size.
dist-apk 0.11.1 now strips with llvm-strip, which it locates itself (mcpp::toolchain_dir()/bin/llvm-strip).
mcpp pack --no-strip ("Ship the artifacts as built") and --debug-symbols <DIR> do not reach that step: a build program receives MCPP_PACK_FORMAT and MCPP_PACK_STAGE_DIR, and nothing about the strip decision.
--format tar / dir for Android, and every desktop pack with a graph-built shared library, still ship symbols.
Proposed (for discussion).
Strip what the graph built: the program on every row, with the SharedLibrary shape when the program is a shared object, and every shared library the graph compiled. Both honour --no-strip and --debug-symbols.
Decide the toolchain's own runtime libraries (libc++_shared.so) explicitly. Gradle strips them. Either way the policy should be stated.
Expose the resolved decision to build programs (for example mcpp::pack_strip() and mcpp::pack_debug_symbols_dir()), so a dist member that re-packs libraries follows the same switch.
CI check (ubuntu-24.04). The fixture above.
mcpp pack --target x86_64-linux-android --format tar gives libapp.so and libdep.so with no .symtab and no .debug_*, and readelf --dyn-syms -W still lists app_entry and dep_answer.
mcpp pack --format tar on hostapp/ gives the same for lib/libdep.so.
With --no-strip, all of them keep .symtab.
E6. A tool behind a feature cannot depend on the package that declares it
Need. HuxerUI's Windows Setup.exe runs an installer interface: a HuxerUI program compiled against the framework with WiX's bootstrapper SDK (<name>-Installer.exe beside mbanative.dll in the bundle).
Its source is the same in every application: a seven-line wWinMain and a build program that names the target. Only the application's name differs.
Today every application carries that three-file windows/installer/ package: four copies across the framework's templates and an example.
The natural shape is the framework providing the program behind a feature. The program depends on the framework, so the declaration is a cycle.
Proposed (for discussion). The tool is already a separate host build, "once per package source and host toolchain", and its graph does not activate fw's installer feature. fw as the tool's dependency (host toolchain, default features) and fw as the application's dependency (target toolchain, installer) are two nodes.
Key a package's build per (package, activated features, toolchain). The recursion then ends: the host fw without installer has no tool edge.
Where the tool graph really does activate the declaring feature, refuse before building the tool, naming the edge.
CI check (ubuntu-24.04). The fixture above:
mcpp build --features windows-installer exits 0;
the application's build program prints a non-empty dep_bin path;
the tool prints fw-installer ran 42.
E7. A git dependency resolves only the repository's root package
Fixture. A git repository whose root package lists a member:
Its build program prints mcpp::dep_bin("fw-installer", "fw-installer").
Measured.
$ mcpp build --features windows-installer
warning: 'app' declares the dependency 'spike.fw-installer', which names spike.fw-installer; the manifest '~/.mcpp/git/2493e8e7594755c7/mcpp.toml' declares spike.fw, and that identity is used.
hint: write 'spike.fw' in 'app' to state the identity the manifest declares.
Resolving toolchain
Resolved gcc@16.1.0 → @mcpp/registry/data/xpkgs/xim-x-gcc/16.1.0/bin/g++
Target x86_64-linux-gnu → x86_64-unknown-linux-gnu
build.mcpp compiling
build.mcpp running
warning: app: SPIKE short=[]
Inferred sources [src/**/*.{cppm,cpp,cc,c,S,s,asm}]
Compiling app v0.1.0 (.)
Compiling spike.fw v
Finished dev [unoptimized + debuginfo] in 0.04s
The git source always yields the root manifest's package, and no key selects another package in the repository. (Separately, the compile line for the git dependency prints spike.fw v with an empty version.)
Need. A framework repository holds several packages: the framework, its rule packages, and tools such as the installer interface in E6.
Before they are published to an index, consumers pin the repository by git revision. HuxerUI's Live2D template and the Lib-Live2D library do so today.
With E6 fixed, such a consumer could name the tool only by path or by published version, not by the git pin the rest of its graph uses.
Proposed (for discussion), either or both:
By identity: when a git dependency's identity differs from the root manifest's, look for it among the root's [workspace] members. The warning above already knows both identities.
By path: a subdir = "tool" key on a git dependency. (Cargo resolves a git dependency by package name within the repository.)
CI check (ubuntu-24.04). A local repository created by the check with git init: spike.fw-installer = { git = "file://…", rev = "…", tools = [...] } builds the member's tool, with no identity warning.
E8. --features activates root features only, and without a [features] table accepts anything
Fixture. E6 without the cycle (the tool does not depend on fw), and the application without a [features] table.
After adding [features] windows-installer = [] to the application:
$ mcpp build --features spike.fw/installer
warning: --features requests 'spike.fw/installer' which [features] does not declare
warning: app: SPIKE short=[] qualified=[]
$ mcpp build --strict --features spike.fw/installer
error: --features requests 'spike.fw/installer' which [features] does not declare
$ echo $?
2
mcpp build --help documents --features <LIST> as "Activate root-package features (comma-separated)", so ignoring a dependency's feature is as documented. Accepting any name, even under --strict, when the root has no [features] table is not.
Need. HuxerUI's CLI runs mcpp pack --format setup --features windows-installer for huxerui package windows, so each application declares that feature. With the tool provided by the framework (E6), each application would keep a windows-installer = ["huxerui.huxerui/windows-installer"] line only so the CLI can name it.
Proposed (for discussion).
Accept <dependency key>/<feature> for a direct dependency, applied exactly as a manifest forward.
Report a name that matches nothing whether or not the root has a [features] table.
CI check.
On the fixture, mcpp build --features spike.fw/installer builds the tool and dep_bin returns its path.
With no [features] table, mcpp build --strict --features nothing-here exits 2.
E9. A machine-readable report of what mcpp pack produced
Measured.mcpp pack reports its artifacts as human status lines. From HuxerUI's conformance runs:
The path goes through mcpp::ui::shorten_path: project-relative, @mcpp/…, ~/… or absolute. A reader has to undo that.
A web pack prints one line per file, an iOS pack a directory, and a multi-row pack adds Packed leg lines.
HuxerUI's CLI publishes a pack's artifacts to dist/<platform>/, as its CMake backend does, by parsing these lines.
docs/50's protocol spells machine output --format json, but pack's --format already names the package format. mcpp test has --message-format json; mcpp pack --message-format json is error: unknown option: --message-format.
The stage manifest (docs/50) describes a staged tree, not the artifacts a format produced.
A smaller inconsistency:
$ mcpp build --release
Finished release [optimized] in 0.00s
$ mcpp pack --release --format tar
error: unknown option: --release
$ mcpp pack --dev --format tar
error: unknown option: --dev
build and run accept --release / --dev. pack accepts only --profile (default [build] default-profile, else release). A driver mapping one profile switch onto the three verbs finds this on the pack.
Proposed (for discussion).
A report on stdout, for example mcpp pack --message-format json or a mcpp.pack kind in docs/50's envelope: one record per artifact with the absolute path, the format, the row or rows, and whether it is a file or a directory. Intermediate legs are separate records or left out.
Accept --release and --dev on pack, as on build and run.
CI check.
mcpp pack --format tar --message-format json (or the chosen spelling) prints a record whose path exists.
mcpp pack --release --format tar exits 0.
E10. The C/C++ runtime model of the llvm row on x86_64-windows-msvc (question)
Measured on windows-2022, in HuxerUI's conformance run. mcpp pack resolved llvm@20.1.7 → x86_64-windows-msvc, with no cxx_runtime or linkage key. The application program in the MSI, read with objdump -p:
mcpp (conformance.exe, 5 726 720 bytes) imports system DLLs (ADVAPI32.dll, USER32.dll, d3d11.dll, …) and api-ms-win-core-* API sets. It imports no msvcp140.dll, no vcruntime140.dll, no api-ms-win-crt-* and no ucrtbase.dll, so the C and C++ runtimes are linked into the program. The installer interface in the bundle is the same.
CMake with MSVC (conformance.exe, 1 925 632 bytes) imports MSVCP140.dll, MSVCP140_ATOMIC_WAIT.dll, VCRUNTIME140.dll, VCRUNTIME140_1.dll and api-ms-win-crt-*, and its MSI installs those four DLLs beside it.
The size difference was not examined.
What the docs and the source say.
docs/20-toolchains.md describes the CRT model ("/MD (host-coupled) by default; /MT when either … is written down") under "Native cl.exe builds". "On the MSVC runtime" maps cxx_runtime values to /MT and /MD.
src/build/flags.cppm adds the CRT flag only when isMsvcDialect = (d.id == "msvc"), and the std module build notes that "Non-MSVC dialects yield "" and the command is unchanged". The llvm row therefore gets clang's default, which the imports above show is the static runtime.
Questions.
Is the static runtime the intended default for the llvm row on x86_64-windows-msvc, which docs/20 names as the default toolchain for the MSVC ABI? If so, it would help to state it for that row.
Can cxx_runtime = "host-coupled" or "toolchain-coupled" select the dynamic runtime there (for example through -fms-runtime-lib=dll)? Reading the source, the key does not reach this row; that was not measured.
中文摘要
HuxerUI 给两个构建系统加了一致性验证:用同一个 CLI 分别创建 CMake 与 mcpp 应用、各自打包,比对产物的程序模型事实(六个平台)。修复之后,六个平台在 Sunrisepeak/HuxerUI#7 上全部一致。插件能修的已经发布为
mcpp:plugins0.11.1(mcpp-community/mcpp-plugins#27)。下面是插件替代不了、需要引擎处理的部分,以及插件侧剩下的两个小项。编号接着 #647 的 E1–E4。mcpp pack不剥离 Android 行的程序,也不剥离图内构建的共享库。libapp.so、libdep.so、libc++_shared.so都带.symtab与.debug_*;libdep.so同样没剥离;dep_bin也拿得到路径,随后应用构建报dependency cycle through package 'fw' while computing its build-cache key;[workspace] members)选不到;--features只接受根包 feature。[features]表时,--features dep/feature被警告后忽略,--strict下报错;[features]表时,任何名字都被静默忽略,--strict也不报错;mcpp pack的机器可读产物清单。Packed <路径>文本行,路径还经过缩写(项目相对、@mcpp/、~/);pack不接受--release/--dev,build与run都接受。msvcp140.dll、vcruntime140.dll、api-ms-win-crt-*;/MD,源码里 CRT 选择只作用于msvc方言;cxx_runtime能否在这一行选择动态运行时。NSHighResolutionCapable、LSRequiresIPhoneOS)只能改值,不能去掉;index.html。每一项都附实测输出,以及可以在 GitHub 托管 runner 上跑的 CI 检查。
Context
HuxerUI builds with CMake and with mcpp. Its specification (
docs/design/build-systems-spec.mdon Sunrisepeak/HuxerUI#7) states that a project built with either build system's defaults is the same program.build-systems-conformance.ymlchecks that on every platform:huxerui package <platform>.The first runs found differences in three places.
mcpp:plugins: fixed in 0.11.1 (0.11.1: dist-apk packs native libraries as the Android Gradle plugin does; dist-appimage takes an SVG icon mcpp-plugins#27):dist-apkstrips native libraries and stores them in place when the manifest asks;dist-appimagetakes an SVG icon.Measured with mcpp 2026.9.15.2 and
mcpp:plugins0.11.1, on Linux x86_64 unless stated. Source references are tomainat 2fc7b5b. The fixtures are throwaway directories, every file is quoted, and the home directory is written as~.mcpp packstrips the program on every row and the shared libraries the graph builds--no-strip/--debug-symbolsreaching every packed library--features <dependency>/<feature>; an unknown name is reported with no[features]tablemcpp pack;--release/--devonpackmcpp packproducedx86_64-windows-msvcE5.
mcpp packleaves an Android row's program, and every graph-built shared library, unstrippedFixture. An application with a shared dependency, packed for Android and, with a
bintarget, for the Linux desktop.hostapp/is the same package with[targets.hostapp] kind = "bin"andint main() { return dep_answer() == 42 ? 0 : 1; }.Measured: the Android row.
In the archive, read with
fileandreadelf -S -W:filelib/libapp.so(the program).symtab,.debug_linelib/libdep.so(built by this graph).symtab,.debug_linelib/libc++_shared.so(the NDK's).symtab,.debug_abbrev,.debug_info,.debug_ranges,.debug_str,.debug_line,.debug_locMeasured: the Linux desktop row.
bin/hostappis stripped: no.symtab, no.debug_*.lib/libdep.sois not stripped: it keeps.symtaband 7.debug_*sections (16 592 bytes).In an application. HuxerUI's two-ABI APK, arm64-v8a, before dist-apk 0.11.1 stripped (bytes):
libconformance.so(the program)libhuxerui.so(the framework, a shared library the graph builds)libc++_shared.so(the NDK's)The Android Gradle plugin strips every native library it packages with
llvm-strip --strip-unneeded, the NDK'slibc++_shared.soincluded.Cause, partly.
strip_programinsrc/pack/pack.cppmstrips the staged program with theExecutableshape and nothing else. Its comment gives the reason:That reason holds for store and host files. It does not hold for a shared library this graph compiled from source (
libdep.soabove). On the Android row the program itself is not stripped either, although the line saysstripped; that path was not traced.Why it matters beyond size.
dist-apk0.11.1 now strips withllvm-strip, which it locates itself (mcpp::toolchain_dir()/bin/llvm-strip).mcpp pack --no-strip("Ship the artifacts as built") and--debug-symbols <DIR>do not reach that step: a build program receivesMCPP_PACK_FORMATandMCPP_PACK_STAGE_DIR, and nothing about the strip decision.--format tar/dirfor Android, and every desktop pack with a graph-built shared library, still ship symbols.Proposed (for discussion).
SharedLibraryshape when the program is a shared object, and every shared library the graph compiled. Both honour--no-stripand--debug-symbols.libc++_shared.so) explicitly. Gradle strips them. Either way the policy should be stated.mcpp::pack_strip()andmcpp::pack_debug_symbols_dir()), so a dist member that re-packs libraries follows the same switch.CI check (
ubuntu-24.04). The fixture above.mcpp pack --target x86_64-linux-android --format targiveslibapp.soandlibdep.sowith no.symtaband no.debug_*, andreadelf --dyn-syms -Wstill listsapp_entryanddep_answer.mcpp pack --format taronhostapp/gives the same forlib/libdep.so.--no-strip, all of them keep.symtab.E6. A tool behind a feature cannot depend on the package that declares it
Fixture.
Measured.
The tool is built and runs, and the application's build program receives its path. The build then fails computing
fw's cache key.From the same fixture:
reexport = true,dep_binis empty in the application's build program and the build fails with the same error.fw(no[dependencies], amainthat does not callfw_answer), the build succeeds anddep_binreturns the path.dep_binwith the qualified name is empty in every case (Build programs: query the resolved dependency graph and package metadata; compile Swift sources; Android on a macOS host; feature and tool contract mismatches #647 E4, item 3).Need. HuxerUI's Windows Setup.exe runs an installer interface: a HuxerUI program compiled against the framework with WiX's bootstrapper SDK (
<name>-Installer.exebesidembanative.dllin the bundle).wWinMainand a build program that names the target. Only the application's name differs.windows/installer/package: four copies across the framework's templates and an example.Proposed (for discussion). The tool is already a separate host build, "once per package source and host toolchain", and its graph does not activate
fw'sinstallerfeature.fwas the tool's dependency (host toolchain, default features) andfwas the application's dependency (target toolchain,installer) are two nodes.fwwithoutinstallerhas no tool edge.CI check (
ubuntu-24.04). The fixture above:mcpp build --features windows-installerexits 0;dep_binpath;fw-installer ran 42.E7. A git dependency resolves only the repository's root package
Fixture. A git repository whose root package lists a member:
repo/tool/mcpp.tomlisspike.fw-installer, as in E6, depending onspike.fw = { path = ".." }. The application names both by git revision:Its build program prints
mcpp::dep_bin("fw-installer", "fw-installer").Measured.
The git source always yields the root manifest's package, and no key selects another package in the repository. (Separately, the compile line for the git dependency prints
spike.fw vwith an empty version.)Need. A framework repository holds several packages: the framework, its rule packages, and tools such as the installer interface in E6.
Proposed (for discussion), either or both:
[workspace] members. The warning above already knows both identities.subdir = "tool"key on a git dependency. (Cargo resolves a git dependency by package name within the repository.)CI check (
ubuntu-24.04). A local repository created by the check withgit init:spike.fw-installer = { git = "file://…", rev = "…", tools = [...] }builds the member's tool, with no identity warning.E8.
--featuresactivates root features only, and without a[features]table accepts anythingFixture. E6 without the cycle (the tool does not depend on
fw), and the application without a[features]table.Measured.
After adding
[features] windows-installer = []to the application:mcpp build --helpdocuments--features <LIST>as "Activate root-package features (comma-separated)", so ignoring a dependency's feature is as documented. Accepting any name, even under--strict, when the root has no[features]table is not.Need. HuxerUI's CLI runs
mcpp pack --format setup --features windows-installerforhuxerui package windows, so each application declares that feature. With the tool provided by the framework (E6), each application would keep awindows-installer = ["huxerui.huxerui/windows-installer"]line only so the CLI can name it.Proposed (for discussion).
<dependency key>/<feature>for a direct dependency, applied exactly as a manifest forward.[features]table.CI check.
mcpp build --features spike.fw/installerbuilds the tool anddep_binreturns its path.[features]table,mcpp build --strict --features nothing-hereexits 2.E9. A machine-readable report of what
mcpp packproducedMeasured.
mcpp packreports its artifacts as human status lines. From HuxerUI's conformance runs:mcpp::ui::shorten_path: project-relative,@mcpp/…,~/…or absolute. A reader has to undo that.Packed leglines.dist/<platform>/, as its CMake backend does, by parsing these lines.--format json, butpack's--formatalready names the package format.mcpp testhas--message-format json;mcpp pack --message-format jsoniserror: unknown option: --message-format.A smaller inconsistency:
buildandrunaccept--release/--dev.packaccepts only--profile(default[build] default-profile, elserelease). A driver mapping one profile switch onto the three verbs finds this on the pack.Proposed (for discussion).
mcpp pack --message-format jsonor amcpp.packkind in docs/50's envelope: one record per artifact with the absolute path, the format, the row or rows, and whether it is a file or a directory. Intermediate legs are separate records or left out.--releaseand--devonpack, as onbuildandrun.CI check.
mcpp pack --format tar --message-format json(or the chosen spelling) prints a record whose path exists.mcpp pack --release --format tarexits 0.E10. The C/C++ runtime model of the llvm row on
x86_64-windows-msvc(question)Measured on
windows-2022, in HuxerUI's conformance run.mcpp packresolvedllvm@20.1.7 → x86_64-windows-msvc, with nocxx_runtimeorlinkagekey. The application program in the MSI, read withobjdump -p:conformance.exe, 5 726 720 bytes) imports system DLLs (ADVAPI32.dll,USER32.dll,d3d11.dll, …) andapi-ms-win-core-*API sets. It imports nomsvcp140.dll, novcruntime140.dll, noapi-ms-win-crt-*and noucrtbase.dll, so the C and C++ runtimes are linked into the program. The installer interface in the bundle is the same.conformance.exe, 1 925 632 bytes) importsMSVCP140.dll,MSVCP140_ATOMIC_WAIT.dll,VCRUNTIME140.dll,VCRUNTIME140_1.dllandapi-ms-win-crt-*, and its MSI installs those four DLLs beside it.The size difference was not examined.
What the docs and the source say.
/MD(host-coupled) by default;/MTwhen either … is written down") under "Native cl.exe builds". "On the MSVC runtime" mapscxx_runtimevalues to/MTand/MD.src/build/flags.cppmadds the CRT flag only whenisMsvcDialect = (d.id == "msvc"), and the std module build notes that "Non-MSVC dialects yield "" and the command is unchanged". The llvm row therefore gets clang's default, which the imports above show is the static runtime.Questions.
x86_64-windows-msvc, which docs/20 names as the default toolchain for the MSVC ABI? If so, it would help to state it for that row.cxx_runtime = "host-coupled"or"toolchain-coupled"select the dynamic runtime there (for example through-fms-runtime-lib=dll)? Reading the source, the key does not reach this row; that was not measured.HuxerUI's specification allows this difference for now: CMake installs the MSVC runtime DLLs, and mcpp links the runtime in.
mcpp:pluginsfollow-ups (low priority)dist-apple: a defaulted Info.plist key can change its value but cannot be removed.NSHighResolutionCapableis always written; on iOS,LSRequiresIPhoneOS(dist/apple.cppm, thereplaced(...)checks).dist-web: the page is alwaysindex.html.index.htmlas the default.Already released in 0.11.1 (mcpp-community/mcpp-plugins#27):
dist-apkstrips native libraries (the workaround E5 would make unnecessary);dist-apkstoreslib/uncompressed on 16 KB pages when the manifest statesandroid:extractNativeLibs="false";dist-appimagetakes SVG icons.Still open elsewhere