Skip to content

Platform-defined targets, fewer hard-coded OS/arch facts: 12 engine gaps + 3 bugs measured while porting a C program (cmd) to ok images, FreeBSD and i386 with mcpp only #911

Open

Description

@Sunrisepeak

TL;DR

We ported a real C program — ChenPi11/cmd (a cmd.exe re-implementation for Unix, ~18.7k lines of C89, already shipping an mcpp.toml) — to every platform its README lists, using only mcpp: ok images (x86_64/aarch64-openkal, one .okb), FreeBSD 14.5, and an i386 probe towards SVR4. Most of it worked, several parts beautifully (the platform package, patches, run_under, okb packing). Everything that did not work traces back to one root cause:

Facts about operating systems and architectures are hard-coded in the engine (the closed target vocabulary, per-row flags, host-centred runtime validation, carrier rows, per-OS descriptor blocks) instead of being data that packages (platforms, sysroots, runners) declare.

This issue lists 12 engine gaps (G1–G12) and 3 bugs (B1–B3) found along the way. Each one has evidence (permalinks at 34406d6), a reproduction, the scenario that needs it, and a proposal. The acceptance test is concrete: every workaround listed in §6 disappears.

中文摘要

我们用 mcpp 把一个真实的 C 项目 ChenPi11/cmd 移植到它 README 里列出的所有平台:ok image(x86_64 和 aarch64,合成一个 okb)、FreeBSD 14.5,以及一个面向 SVR4 的 i386 探针。大部分都跑通了,其中 platform 包、patches、run_under、okb 打包这几项体验很好。

所有卡住的地方都指向同一个根因:系统和架构的事实被写死在引擎里(封闭的 target 词汇表、各行强加的参数、以本机为中心的运行时校验、carrier 行、描述符的平台块),而不是由 platform、sysroot、runner 这些包作为数据来声明。

下面按 G1–G12(引擎能力缺口)和 B1–B3(顺带发现的 bug)逐条给出:证据(固定到 34406d6 的源码链接)、复现、需要它的场景,以及建议。验收标准是:§6 列出的每一个绕路做法都可以删除。

1. Context

mcpp 2026.11.7.2 (source read at 34406d6, which is on origin/main)
toolchain [email protected] (xim)
openkal family openkal-platform 0.24.3, openkal-run/tools 0.5.3, openkal-musl 0.28.1, openkal 0.24.0
host Linux x86_64; FreeBSD 14.5-RELEASE (official VM image) under qemu/KVM for the run tests
project ChenPi11/cmd @ 1edab21 — C89, POSIX layer l*.c, Win32 layer lw*.c, two programs (cmd.exe, COMMAND.COM)

mcpp itself was not modified. Everything below was measured, not reasoned — except where marked (documented, not exercised).

2. What already works (please keep it)

# Result
E2 cmd's C sources build unchanged for x86_64-openkal. Its batch-script regression output is identical to the native build (only MKLINK /H differs: ENOSYS, as openkal-musl documents)
E3 openkal-posix (an openkal implementation over the host's POSIX libc, 942 lines) plus upstream openkal-run with one patch (§G7) builds a working okrun via mcpp. mcpp reports kernel-abi openkal (openkal-posix, graph). Output is byte-identical to the official okrun
E4 A platform package (targets = ["x86_64-none-elf"] plus flags) makes unmodified mcpp emit a static FreeBSD 14.5 ELF — and the okrun above, built for FreeBSD, runs the same cmd.oke built on Linux inside a FreeBSD VM: pipes, FOR /F, batch files, interactive line editing
E6 C++23 named modules plus import std in an ok image run identically under the official okrun and okrun-posix
E8 aarch64-openkal runs through MCPP_TARGET_AARCH64_OPENKAL_RUN_UNDER=qemu-aarch64-static, which packs the linux-aarch64 carrier automatically
E9 mcpp pack --target openkal --pkg-format okb produces one 2.9 MB okb (x86_64 + aarch64) that runs directly
— Value provenance in reports (Resolved … ← source, (platform), (graph)), patch variants recorded in lock and SBOM, layer provider resolution — all excellent

We also checked, and these are not gaps: the engine already passes --no-default-config on the compile and link lines (the xlings clang .cfg only bites when clang is called by hand), and multi-architecture okb packing exists.

3. Principle proposed

Mechanism in the engine, facts in packages. The engine owns the axes (arch, os, env, object format, C environment, execution host) and the protocols (merge order, provenance, validation, packing). It should not own the values — which OSes exist, which code model a row uses, which loader a program has, which systems a carrier serves.

mcpp already does exactly this for the two openkal rows, where a target is served by a versioned package (openkal-platform). This issue asks to generalise that model: any target may be defined by a package, and the built-in table becomes the index of default platform packages.

4. Engine gaps

G1 — The target vocabulary is closed: a platform cannot define a target

  • Observed: a triple whose OS segment the parser does not know is unparseable — x86_64-freebsd, i686-svr4, x86_64-haiku. So cfg(os = "freebsd") can never hold, and a platform can only serve existing rows. For FreeBSD we had to borrow x86_64-none-elf and restate the real target in compile flags. The vocabulary has no 32-bit x86 row at all.
  • Evidence: triple.cppm#L12 (os ∈ {linux, macos, windows}); triple.cppm#L1515 (an unrecognized segment yields nullopt). The [target.X] escape hatch only helps for parseable triples.
  • Scenario: BSDs, illumos, Haiku, Hurd and SVR4 are each served by one small platform package (sysroot, clang triple, link conventions). Today every one of them must masquerade as a bare-metal row, which drags in G2–G5.
  • Proposal: a platform package may declare [[platform.define]] rows: triple, arch, os, env, clang-triple, object-format, data-model, exe-suffix, loader/interp, code-model, link-emulation, descriptor-key. The parser accepts any os/env token that some resolved platform defines. cfg(os = …) evaluates against the defined row.

G2 — Flags the engine injects per row cannot be replaced, and their provenance is invisible

  • Observed: for an i386 target served through x86_64-none-elf, the row's -mcmodel=small is rejected by clang for every i386 spelling (i386-unknown-none-elf, i386-unknown-linux-gnu, i386-pc-solaris2.11, x86_64 … -m32): error: unsupported argument 'small' to option '-mcmodel='. Platform flags can only be appended (SPEC-013 §6), so the only way through was a toolchain wrapper (MCPP_TOOLCHAIN=path:… whose clang drops that one argument). With it, mcpp produced and ran an ELF 32-bit LSB executable, Intel 80386, statically linked (E7). The same row also injects -m elf_x86_64 -Bstatic --no-dynamic-linker.
  • Evidence: target.cppm#L241 (row: x86-64 / sysv / small / elf_x86_64); target.cppm#L365; linkline.cppm#L124-L134.
  • Scenario: any target whose facts differ from the row it rides on: i386, x32, a kernel at -mcmodel=kernel.
  • Proposal: (a) the engine's row defaults are data with a source, replaceable by a more specific declarer (platform, then project) with an explicit replace, while conventions stay append-only; (b) mcpp explain flags <unit> prints every flag with the layer that contributed it. With G1 this mostly dissolves, because the row would be the platform's own.

G3 — Hostedness has two answers; the sysroot's declaration is ignored

  • Observed: platform sysroot = { path = "<FreeBSD base>", provides = ["mcpp:c-abi=freebsd-libc", "hosted-standard-library"] } is accepted and nicely reported (c-abi sysroot (path:…, prebuilt)), but -ffreestanding is still added (verified in build.ninja). A C++ int main is then mangled (undefined symbol: main … did you mean extern "C"), so we had to append -fhosted.
  • Evidence: target.cppm#L413: if (!targetCxxRuntime) out.emplace_back("-ffreestanding"); — only a graph C++ runtime counts.
  • Proposal: hostedness is one declared fact of the C environment. A sysroot, c-abi provider or platform that declares hosted-standard-library (or hosted = true) turns off -ffreestanding and the freestanding treatment of main.

G4 — link.driver = "compiler" is not honoured on rows that link with ld.lld directly

  • Observed: with [platform.link] driver = "compiler", the link line is still ld.lld -m elf_x86_64 -Bstatic --no-dynamic-linker --gc-sections …. So the platform must re-implement what clang's FreeBSD driver already knows: crt1.o/crti.o/crtbeginT.o before the objects, crtend.o/crtn.o as link.inputs after them, the library group, and --sysroot again for lld (FreeBSD's libc.so is a linker script naming /lib/libc.so.7).
  • Evidence: ninja_backend.cppm#L1595 treats link.driver only as an opt-in to "lld"; the row's lldEmulation path (linkline.cppm#L124) wins otherwise.
  • Proposal: the link model is platform data and is honoured on every row. With driver = "compiler", --target and a sysroot, clang's own driver for that OS supplies the start files and libraries. Don't re-encode, per package, knowledge the compiler driver already has.

G5 — Runtime closure validation assumes every ELF with PT_INTERP is a program for this host

  • Observed: a correct dynamically linked FreeBSD executable (interpreter /libexec/ld-elf.so.1, for FreeBSD 14.5) fails the build: runtime closure validation failed (proven Linux ELF defect) / rule B: … uses PT_INTERP /libexec/ld-elf.so.1 but RuntimeBinding [email protected] selects …/ld-linux-x86-64.so.2. MCPP_ALLOW_HOST_LIBS=1 does not help. We were forced to link statically. Haiku cannot link statically at all, so it is blocked outright.
  • Evidence: runtime_validation.cppm#L651 and #L683 (the escape is the triple's has_own_sysroot(), which a platform cannot set); ninja_backend.cppm#L5151. Same family as A stated sysroot with its own glibc: link fails, the artifact loads the host's loader, rule B compares the wrong glibc #833.
  • Proposal: validate against the artifact's declared execution environment (from the G1 row or the platform). For a foreign OS: validate the closure against that platform's sysroot and loader, or report "not for this host — closure not checked". Never call it a defect of this host.

G6 — Source-only packages are resolved through per-host-OS descriptor blocks

  • Observed: error: dependency 'openkal': index entry has no versions for platform 'none'. The tarball is identical in every block; we needed descriptor_keys = ["linux"] on the platform.
  • Evidence: specs/platforms.md#L71 (closed key set).
  • Proposal: descriptors separate the source artifact (one, target-independent) from prebuilt payloads (per target, matched on the G1 axes with fallbacks and any). A new platform then needs no descriptor_keys, and the key set stops being a closed list.

G7 — Capability slots with default providers for ordinary packages

  • Observed: openkal-run hard-wires its implementation per OS ([target.linux.dependencies] openkal-linux = …, and the same for macOS and Windows). To use another implementation of mcpp:kernel-abi=openkal, the consumer must patch the package (our 35-line patch removes those tables). For a FreeBSD build no table matches, so nothing is selected.
  • Scenario: this is exactly what mcpp already solves for platform layers ("a graph package that provides the layer takes the platform's place", SPEC-013 §7.2). Ordinary packages need the same thing.
  • Proposal: a package declares requires = ["mcpp:kernel-abi=openkal"] plus [defaults."mcpp:kernel-abi"] linux = "openkal-linux@^0.25" …. A provider already in the graph, chosen by the consumer or brought by the platform, takes precedence. No patch is needed, and new systems need no change in the consuming package.

G8 — Patch root: what a patch must name is the store layout, not the package

  • Observed: for openkal-run (descriptor mcpp = "*/run/mcpp.toml"), a patch naming a/mcpp.toml or a/run/mcpp.toml fails with '…' is not in the package. It works only as a/openkal-image-0.5.3/run/mcpp.toml: the root is the store's version directory, which contains the archive's top-level directory. A patch therefore embeds the version in its paths and cannot be reused across versions; the author has to inspect ~/.mcpp/xpkgs to write it.
  • Evidence: patch.cppm#L248, #L328; docs: 05-dependencies.md#L651.
  • Proposal: the patch root is the package root (the directory holding the resolved manifest), or an explicit patch_root. Also add mcpp patch edit <dep> / mcpp patch save <dep> (edit the variant tree, emit the diff) — the workflow is otherwise hand-made.

G9 — Per-artifact compile variants of shared sources

  • Observed (in the project itself): cmd's mcpp.toml says "mcpp compiles shared sources once, so the entry file is the only place a per-target define can take effect". Its authors added a mcpp/cmain_command.c that #includes ../cmain.c, just to give COMMAND.COM -DENABLE_AUTOEXEC=1. Every other build system in that repo (CMake, Meson, xmake, Make) compiles the sources once per program.
  • Evidence: cmd mcpp.toml#L7-L11.
  • Proposal: [targets.X] defines (and flags) may apply to the shared sources of that artifact. The engine compiles a variant of each affected unit, keyed by its flags, only where the flags differ.

G10 — Carriers and packs exist only for the built-in rows (documented, not exercised)

  • SPEC-013 §11 fixes the carrier's rows to <arch>-linux-musl, <arch>-macos and <arch>-windows-gnu (specs/platforms.md#L349). The FreeBSD okrun we built cannot become a single-file carrier or a --systems freebsd pack.
  • Proposal: any row a resolved platform can build may host a carrier or pack, as that platform declares.

G11 — Execution hosts beyond user-mode emulation

  • Observed: run_under works well for qemu-user (E8). Running on FreeBSD needed a hand-written driver: boot the qemu/KVM image, move the loader console to serial, log in, mount a virtual FAT disk, copy, run, collect output, power off.
  • Proposal: an execution-host protocol — transfer artifact(s) → run argv → stream output → exit status — implemented by distributable runner packages (system VM, ssh, device; docs/41 already has the device half). Then mcpp test --target x86_64-freebsd works in CI.

G12 — A missing 32-bit x86 row blocks a whole class of targets

Kept separate from G1 because it is valuable even without G1. An i686-none-elf row (empty code model, elf_i386) would let a platform reach i386 SVR4, i386 Hurd and other legacy x86 systems. It is the single blocker found for the openkal "linked arrangement" (implementation linked into a native static program) on SVR4.

5. Bugs found along the way (happy to split them into separate issues)

B1 — Generated manifests do not quote target names containing .. mcpp pack on a package with [targets."COMMAND.COM"] writes target/launcher-pack/COMMAND.COM/mcpp.toml containing:

[targets.COMMAND.COM]
kind = "bin"

which reparses as table COMMAND → COM: error: targets.COMMAND.kind missing or not a string. The same format string appears at pack.cppm#L2561, manifest_emit.cppm#L197, distribution.cppm#L1076 and #L1423. Fix: emit TOML-quoted keys.

B2 — A project's artifact_name override defeats the platform's image naming. cmd's build.mcpp calls mcpp::artifact_name("exe", "{name}") to suppress the host suffix. For x86_64-openkal this also removes .oke, and the platform's own okb step fails: okld: …/lib/aarch64/COMMAND.COM: an image is named <name>.oke or <name>.okso. Expected: a platform-defined format suffix (link.exe) is not overridden by a host-suffix rule, or the conflict is reported at plan time.

B3 — mcpp pack silently packs one program of several. With package cmd and programs cmd and COMMAND, mcpp pack (no target named) packed only COMMAND. This held for both okb and tar, with no warning; mcpp pack cmd packs cmd. The selection is the package-name match, then "the first binary target" (pipeline.cppm#L532-L548), and neither branch is reported. That the package-name branch did not pick cmd here was not investigated further. Expected: refuse or warn when several programs qualify, and offer a way to ship all programs of a package in one distribution (cmd needs cmd.exe and COMMAND.COM together).

6. Acceptance criteria: the workarounds that should disappear

Workaround used in this port Removed by
Borrowing x86_64-none-elf to build a FreeBSD program G1
Toolchain wrapper that strips -mcmodel=small for i386 G2 / G12
-fhosted appended to undo -ffreestanding G3
Hand-written crt1/crti/crtbeginT … crtend/crtn plus library group in ldflags and link.inputs G4
Forced static linking (and Haiku not possible) G5
descriptor_keys = ["linux"] G6
35-line patch of openkal-run removing its per-OS implementation tables G7
Patch paths naming openkal-image-0.5.3/run/… G8
mcpp/cmain_command.c wrapper in cmd G9
Hand-written qemu serial driver for testing on FreeBSD G11

Suggested priority: G1, G2/G12, G7 first (they unblock and remove most of the rest), then G3–G5 and G9, then G6, G8, G10 and G11. B1–B3 are small and independent.

7. Out of scope for mcpp (for routing; we will raise these upstream separately)

  • openkal-image: libokld accepts only EM_X86_64 and EM_AARCH64 ELF64 images. An i386 ok image needs ELF32/REL support and an image_abi.h branch (spec clause 12 says "of the architecture's class", so the format is ready).
  • openkal-run: bind only the interfaces an implementation provides (today every surface name is bound, which forces stub functions in a partial implementation), plus G7 on its manifest.
  • openkal: no asynchronous interrupt notification. Under okrun a shell cannot survive Ctrl+C while a child runs (measured with both runners).

Appendix — minimal reproductions

A. FreeBSD through a platform package (G1, G3, G4, G5, G6)
# platforms/freebsd/mcpp.toml   (SR = an extracted FreeBSD 14.5 base.txz: usr/include, usr/lib, lib)
[package]
name = "freebsd-platform"
version = "0.0.1"

[platform]
targets = ["x86_64-none-elf"]              # G1: no x86_64-freebsd row can be defined
toolchain = "[email protected]"
descriptor_keys = ["linux"]                # G6

[platform.layers]
"c-abi" = false
"c++-abi" = false
"compiler-runtime" = false
"kernel-abi" = false

[platform.build]
cflags   = ["-fhosted", "--target=x86_64-unknown-freebsd14.5", "--sysroot=SR"]   # -fhosted: G3
cxxflags = ["-fhosted", "--target=x86_64-unknown-freebsd14.5", "--sysroot=SR",
            "-nostdinc++", "-isystem", "SR/usr/include/c++/v1"]
# G4: driver = "compiler" is ignored, so ld.lld's own terms; static because of G5
ldflags  = ["--sysroot=SR", "--eh-frame-hdr", "-LSR/usr/lib", "-LSR/lib",
            "SR/usr/lib/crt1.o", "SR/usr/lib/crti.o", "SR/usr/lib/crtbeginT.o",
            "--start-group", "-lc++", "-lcxxrt", "-lthr", "-lc", "-lm", "-lgcc", "-lgcc_eh", "--end-group"]

[platform.link]
inputs = [{ file = "crt/crtend.o" }, { file = "crt/crtn.o" }]
mcpp build --target x86_64-none-elf --platform ./platforms/freebsd
# → ELF 64-bit LSB executable, x86-64, statically linked, for FreeBSD 14.5 (NT_FREEBSD_ABI_TAG 1405000)
# A dynamic link instead (ldflags "-Bdynamic", "--dynamic-linker=/libexec/ld-elf.so.1", crtbegin.o, -lc …)
# links correctly (interpreter /libexec/ld-elf.so.1) and is then refused:
# → error: runtime closure validation failed (proven Linux ELF defect)   (G5)
B. i386 (G2, G12)
[platform]
targets = ["x86_64-none-elf"]
toolchain = "[email protected]"

[platform.build]
cflags  = ["-fhosted", "--target=i386-unknown-none-elf", "-march=i386", "-fno-pic"]
ldflags = ["-m", "elf_i386", "-e", "_start"]
clang: error: unsupported argument 'small' to option '-mcmodel=' for target 'i386-unknown-none-elf'

With MCPP_TOOLCHAIN=path:<llvm 23 with a clang wrapper dropping -mcmodel=small for i386>:
ELF 32-bit LSB executable, Intel 80386, statically linked — the program runs (int $0x80 probe).

C. The openkal-run patch needed today (G7, G8)
--- a/openkal-image-0.5.3/run/mcpp.toml
+++ b/openkal-image-0.5.3/run/mcpp.toml
@@ -14,30 +14,11 @@
 openkal = "^0.24.0"
 openkal-ld = { path = "../ld" }
 
-[target.linux.dependencies]
-openkal-linux = "^0.25.0"
+# The implementation is the consumer's choice (patched for openkal-posix).
 
-[target.macos.dependencies]
-openkal-macos = "^0.22.1"
-
-[target.windows.dependencies]
-openkal-windows = "^0.20.0"
-
-# The interfaces each system's implementation provides decide one unit: where
-# openkal.space is provided its names are bound, and elsewhere an image that
-# requires it is refused at load, naming it.
 [build]
-sources = ["src/okrun.cppm", "src/run.cpp", "src/bindings.cpp", "src/seal.cpp", "src/prof.cpp"]
+sources = ["src/okrun.cppm", "src/run.cpp", "src/bindings.cpp", "src/seal.cpp", "src/prof.cpp", "src/bindings_space.cpp"]
 include_dirs = ["include"]
 flags = [
   { glob = "src/*.cpp", cxxflags = ["-fno-exceptions", "-fno-rtti"] },
 ]
-
-[target.linux.build]
-sources = ["src/bindings_space.cpp"]
-
-[target.macos.build]
-sources = ["src/bindings_space.cpp"]
-
-[target.windows.build]
-sources = ["src/no_space.cpp"]
[dependencies]
openkal-run   = { version = "0.5.3", patches = ["patches/0001-openkal-run-implementation-chosen-by-consumer.patch"] }
openkal-posix = { path = "../openkal-posix" }   # provides = ["mcpp:kernel-abi=openkal"]

Activity

  1. Sunrisepeak commented on Nov 9, 2026

    @Sunrisepeak
    MemberAuthor

    Re-measured on 2026.11.9.1: most of G1–G12 / B1–B3 hold; 8 follow-up observations (N1–N8)

    Thank you for #912. That was a very fast turnaround. I re-ran the whole port with the released 2026.11.9.1 (linux-x86_64 tarball, llvm 23.1.3). For openkal-run 0.5.4 I used git = "https://github.com/mcpplibs/openkal-image.git", tag = "0.5.4", since mcpplibs/mcpp-index#743 is not merged yet. Below is what I measured, followed by a few follow-up observations. The observations come with workarounds that already work today, and a possible direction for each. They are offered for consideration only. How or whether to change anything is of course your call.

    Permalinks are pinned to the v2026.11.9.1 tag commit 9edccc4.

    中文摘要

    用 2026.11.9.1 发布版把移植实验完整重跑了一遍。G1–G4、G6、G7、G11、G12 和 B1–B3 实测都已生效。cmd 现在能不改源码直接编译成 FreeBSD 原生程序。借助一个运行器脚本,mcpp run --target x86_64-freebsd 能在真实的 FreeBSD 14.5 虚拟机里执行并返回退出码。

    另外观察到 8 个后续问题(N1–N8),每个都附上了现在就能用的绕过方式和一个可供参考的思路,不要求上游按此修改:

    • N2、N3 的链接部分和 N7 似乎同源:平台定义的行已在运行时登记,但 has_own_sysroot()、freestanding::resolve() 等判断仍只查内置表。
    • N1:共用同一入口文件的两个程序会共享一个入口对象,其中一个程序的 defines 会被悄悄带进另一个。
    • 其余几项较小:N4 是误报,N5 是 git 工作区成员的补丁问题,N6 缺一条警告,N8 是打包问题。

    另外更正 #911 第 7 节的一处说法:openkal 其实有 kal_process_stop_requested(),cmd 调用一次即可挺过 Ctrl+C。那一条不是缺口。


    1. What now works (measured)

    Item Result How it was checked
    G1 ✅ [[platform.define]] x86_64-freebsd + [[platform.provider]]. The build goes to target/x86_64-freebsd/, cfg(os = "freebsd") applies, and mcpp why platform lists the row.
    G2 ✅ mcpp why flags was the tool that located N3 below. Very useful.
    G3 ✅ A hosted = true provider serving x86_64-none-elf: -ffreestanding is gone.
    G4 ✅ driver = "compiler" on the FreeBSD row: crt objects, libraries and PT_INTERP come from the driver. The hand-written crt list is gone.
    G5 ✅ (see N2, N4) "the artifacts are for freebsd, not this machine: their runtime closure is not checked here"
    G6 ✅ No descriptor_keys. Index dependencies (openkal, openkal-run) resolve for x86_64-freebsd.
    G7 ✅ openkal-run 0.5.4, unpatched, takes the graph's openkal-posix as mcpp:kernel-abi. The lock has no openkal-linux, and the runner's output is byte-identical to the official okrun.
    G8 ✅ for index archives (see N5) A patch written as a/mcpp.toml applies to openkal-run 0.5.3 from the index.
    G9 ✅ for shared sources (see N1) [targets."COMMAND.COM".build] defines compiles the 73 shared units under obj/_variant/COMMAND_COM/.
    G10 partly (see N8) mcpp pack --target x86_64-freebsd --mode static packs.
    G11 ✅ runner = ["<vm-runner>", "{}"] boots a FreeBSD 14.5 VM, runs the program and returns its status. mcpp run cmd.exe --target x86_64-freebsd -- /d /c "ver & exit /b 3" ends with 3.
    G12 ✅ --target i686-none-elf builds an ELF32 i386 program that runs, with no toolchain wrapper. why flags shows row -march=i686 and row -m elf_i386.
    B1 ✅ [targets."COMMAND.COM"] in the generated launcher manifest
    B2 ✅ cmd.exe.oke, with the conflict reported
    B3 ✅ The note names the programs not packed; --all-programs gives an okb with 2 architectures × 2 images.

    The following ran in a real FreeBSD 14.5 VM, all dynamically linked:

    • a C program;
    • a C++ program (exceptions, iostream; libc++.so.1, libcxxrt.so.1);
    • cmd itself, built unchanged for x86_64-freebsd, which passes the same batch test as the Linux build;
    • an okrun over openkal-posix built for FreeBSD, running the 9.1-built cmd.exe.oke with the same output (except hard links, which openkal does not have).

    Of the workarounds in the §6 acceptance table:

    • Removed: borrowing x86_64-none-elf, the i386 toolchain wrapper, -fhosted, the hand-written crt list, and descriptor_keys. The VM driver is still there, but it is now plugged in as a runner, so testing goes through mcpp run (G11).
    • Mostly removed: the openkal-run patch, and the archive-rooted patch paths. A FreeBSD build of openkal-run still needs a 3-line patch, but that part belongs to openkal-image.
    • Still in place: cmd's entry wrapper (N1). Forced static linking became optional, but a dynamic build meets N2/N3.

    2. Follow-up observations

    The table gives my own reading of each item. A ✓ in the last column means a workaround that does not change mcpp was verified here.

    # Summary My reading Workaround
    N1 Two programs with the same main file share one entry object compiled with one program's defines defect (silent) ✓ separate entry file per program
    N2 The hermetic check refuses a defined row's dynamic program because of its target-side loader path defect ✓ static link, or MCPP_ALLOW_HOST_LIBS=1
    N3 The payload's libc++ headers and host -L/-rpath reach a defined row with a sysroot partly usage, partly gap, partly defect ✓ a c++-abi layer package; static link avoids the RPATH
    N4 The interposition warning compares a FreeBSD program with host glibc defect (warning only) ✓ ignore
    N5 patches on a git workspace member breaks the member's path edges defect ✓ use the index archive
    N6 [platform.link] flags is silently unused with driver = "compiler" documented; a diagnostic may help ✓ [platform.build] ldflags
    N7 A defined row's march / link-emulation are read but not applied defect (vs SPEC-016 §2.1) ✓ state the flags in the platform's build / link.flags
    N8 Default pack runs host ldd on a foreign dynamic ELF; --mode static labels a dynamic program -static gap + defect ✓ static link

    A possible common cause of N2, N3 (link side) and N7

    The defined row is registered at run time with all its fields (prepare/platform.cpp#L185-L215), and selection, reports and cfg all use it. However, a few predicates further down still read only the compiled-in tables:

    A direction you may wish to consider: let these predicates ask the resolved row and provider (a declared sysroot, execution ≠ this machine, the row's machine description) rather than the compiled-in tables. One change there might settle three observations at once. SPEC-016 already reads that way: §2.1 says the machine description is "引擎原样使用" (used by the engine as written), and §5 says "平台定义的行不取宿主载荷的 C 库" (a platform-defined row does not take the host payload's C library). I have not verified that the llvm payload's -L …/lib/x86_64-unknown-linux-gnu in N3 comes from the same place, so please treat that part as a guess.

    N1 — one entry object for two programs that share main

    [build]
    sources = ["*.c", "!cmain.c"]
    
    [targets."cmd.exe"]
    kind = "bin"
    main = "cmain.c"
    
    [targets."COMMAND.COM"]
    kind = "bin"
    main = "cmain.c"
    defines = ["ENABLE_AUTOEXEC=1"]      # same with [targets."COMMAND.COM".build] defines

    build.ninja contains one obj/cmd/cmain.o with -DENABLE_AUTOEXEC=1, linked into both bin/cmd.exe and bin/COMMAND.COM, and nothing is reported. The cause appears to be entry_object: it reuses the unit by source path, and entryFlagsApplied applies the first program's flags once. With the [targets.X.build] spelling, the 73 shared sources do get their variant, but the entry stays shared. That is why cmd still needs its mcpp/cmain_command.c wrapper.

    Possible directions: when two programs give one entry different flags, compile one entry object per program (as G9 already does for shared sources), or refuse and name both programs. Either would avoid the silent mix. Separately, docs/04-mcpp-toml.md#L544-L546 still says a shared source is never compiled two different ways, which G9 now does.

    N2 — hermetic check vs a target-side loader

    A dynamic program for the FreeBSD row (with sysroot = { path = … }) stops at:

    error: hermetic link check failed — the sandbox toolchain resolves its C runtime outside the sandbox:
             /libexec/ld-elf.so.1 (outside the sandbox)
    

    /libexec/ld-elf.so.1 is the target's PT_INTERP, the same situation that the comment above hermetic.cppm#L246 describes for Android. Today every consumer of such a platform needs MCPP_ALLOW_HOST_LIBS=1 or [build] allow_host_libs, which reads as "link host libraries" although nothing from the host is linked. Static linking avoids the check entirely (verified). Direction: see the common cause above.

    N3 — what a hosted defined row receives from the toolchain payload

    With only sysroot = { path = "sysroot", provides = ["mcpp:c-abi=freebsd-libc"] } (also tried "hosted-standard-library" and "mcpp:c++-abi=libc++"), the report shows c++-abi —, and mcpp why flags shows:

    toolchain -isystem<llvm>/include/c++/v1                  # also on C units: '__config_site' file not found
    engine    -stdlib=libc++ --rtlib=compiler-rt --unwindlib=libunwind
    toolchain -L<llvm>/lib/x86_64-unknown-linux-gnu  -Wl,-rpath,<same>
    toolchain -L<llvm>/lib                           -Wl,-rpath,<same>
    engine    -L<gcc-runtime>/lib64                  -Wl,-rpath,<same>
    

    What works today (usage): a c++-abi + compiler-runtime layer package for the system's own libc++. With it, cxxFromGraph holds and the payload's headers leave the compile (hostflags.cppm#L411-L419). It needed two details:

    • Library by file path. The package names the library by absolute file (<SR>/usr/lib/libc++.so, or libc++.a + libcxxrt.a for static), because a -lc++ resolves to the payload's Linux libc++ first: the host -L precede the package's flags.
    • Two platform packages. Static and dynamic needed one platform package each.

    The three host paths remain in the FreeBSD program's RPATH.

    Possible directions, smallest first:

    • the link side might follow the common cause above;
    • a sysroot (or a hosted provider) might be allowed to state that it holds the C++ run-time and the compiler run-time too, i.e. that it is the whole target side, as the MinGW pairing already does for its payload. That likely touches SPEC-002's c-abi exclusivity rule, so it may deserve its own discussion.

    N4 — interposition warning against host glibc

    For the FreeBSD C++ program, symbol_provision.cppm#L271 reports environ and __progname as "also provided by <glibc payload>/lib/libc.so.6". The program loads libc.so.7 from FreeBSD. Direction: skip the check where the row's execution is not this machine, or resolve DT_NEEDED inside the declared sysroot. It is only a warning, so it is low priority.

    N5 — patches on a git dependency that is a workspace member

    openkal-run = { git = "https://github.com/mcpplibs/openkal-image.git", tag = "0.5.4",
                    patches = ["patches/0001-openkal-run-freebsd-space.patch"] }   # a/mcpp.toml, from the package root
    error: path dependency 'openkal-ld' (at '<home>/cache/src/mcpplibs.openkal-run/ld') has no mcpp.toml
    

    For a member, dep_root is gitRoot / member, and graph.cpp#L1677-L1692 materializes only that directory. The member's openkal-ld = { path = "../ld" } then points outside the variant. The same patch on the index archive works, because the whole archive is copied.

    Direction: copy the clone root into the variant and keep the member's relative position. Alternatively, resolve the member's path edges against the original clone, since §"A package of a git repository" already says such an edge names the same git source at the same commit.

    N6 — [platform.link] flags with driver = "compiler"

    This is documented (specs/platforms.md#L282: only for driver = "lld"), and [platform.build] ldflags = ["-pthread"] is the right spelling. I only mention it because the value was dropped without a word. A manifest warning, in the style of the other platform-statement checks, might save the next author a link failure.

    N7 — a defined row's machine description

    [[platform.define]]
    triple         = "i386-svr4"
    clang-triple   = "i386-unknown-none-elf"
    object-format  = "elf"
    execution      = "svr4"
    march          = "i386"
    link-emulation = "elf_i386"

    mcpp why flags --target i386-svr4 shows no row layer at all (no -march=i386). With driver = "lld" the link line is -flavor gnu --compress-debug-sections=zlib with no -m elf_i386. The program still comes out as ELF32, because lld infers the emulation from the objects. The built-in i686-none-elf gets both flags. Stating them in the platform's build.cflags and link.flags works today. Direction: see the common cause above.

    N8 — packing a defined row's dynamic program

    • mcpp pack --target x86_64-freebsd (vendored): error: ldd failed on …/bin/COMMAND.COM: command exited with 127. Only an ELF without PT_INTERP and without DT_NEEDED skips ldd (pack.cppm#L3163-L3173).
    • --mode static produces cmd-0.1.0-x86_64-freebsd-static.tar.gz whose program is still dynamically linked (interpreter /libexec/ld-elf.so.1). program_row labels a defined row static without changing the link.

    Possible directions:

    • for a row whose execution is not this machine, either compute the closure from DT_NEEDED and the declared sysroot without running anything, or refuse vendored mode with a message that names --mode static / static linking;
    • for --mode static, check the produced program or refuse when the row's provider does not link statically.

    With static linking (ldflags = ["-pthread", "-static"] in the platform) the default mcpp pack already succeeds.


    3. A correction to §7 of the issue

    §7 said "openkal: no asynchronous interrupt notification; under okrun a shell cannot survive Ctrl+C". That is not right. openkal has kal_process_stop_requested() (0.11; openkal-linux takes over SIGINT/SIGTERM/SIGHUP). Calling it once at the top of cmd's main under __OPENKAL__ makes the ok image survive Ctrl+C as the native build does. So that item is a porting step in cmd, not an openkal gap. Sorry for the noise.

    Appendix — the FreeBSD row as measured (dynamic)
    # freebsd-platform/mcpp.toml   (sysroot → an extracted FreeBSD 14.5 base.txz)
    [package]
    name    = "freebsd-platform"
    version = "0.1.0"
    mcpp    = ">=2026.11.9.1"
    
    [platform]
    toolchain = "[email protected]"
    sysroot   = { path = "sysroot", provides = ["mcpp:c-abi=freebsd-libc"] }
    
    [[platform.define]]
    triple        = "x86_64-freebsd"
    clang-triple  = "x86_64-unknown-freebsd14.5"
    object-format = "elf"
    family        = "unix"
    execution     = "freebsd"
    
    [[platform.provider]]
    serves = ["x86_64-freebsd"]
    hosted = true
    
    [platform.build]
    ldflags = ["-pthread"]          # FreeBSD keeps POSIX threads in libthr
    
    [platform.link]
    driver = "compiler"
    
    [platform.layers]
    "kernel-abi"       = false
    "c-abi"            = false
    "c++-abi"          = { path = "../freebsd-cxx" }
    "compiler-runtime" = { path = "../freebsd-cxx" }
    # freebsd-cxx/mcpp.toml — the system's own libc++ (over libcxxrt) and libgcc
    [package]
    name     = "freebsd-cxx"
    version  = "0.1.0"
    provides = ["hosted-standard-library", "mcpp:c++-abi=libc++", "mcpp:compiler-runtime=libgcc"]
    
    [targets.freebsd-cxx]
    kind = "lib"
    
    [build]
    sources      = []
    include_dirs = ["../freebsd-platform/sysroot/usr/include/c++/v1"]
    # By file: a -lc++ would find the toolchain payload's Linux libc++ first (N3).
    ldflags      = ["<SR>/usr/lib/libc++.so"]     # static variant: libc++.a + libcxxrt.a
    # a project
    [target.x86_64-freebsd]
    platform = { path = "../freebsd-platform" }
    runner   = ["../vm/fbrun.sh", "{}"]            # boots the VM, runs, returns the status (G11)

    Happy to re-test any of these on a branch, or to split items into separate issues if that is easier to track.

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