Skip to content

0.6.0: a third family for what comes out of the link, and a table shape for tools-embed - #16

Merged
Sunrisepeak merged 5 commits into
mainfrom
feat/dist-members-and-embed-table
Sep 11, 2026
Merged

0.6.0: a third family for what comes out of the link, and a table shape for tools-embed#16
Sunrisepeak merged 5 commits into
mainfrom
feat/dist-members-and-embed-table

Conversation

@Sunrisepeak

Copy link
Copy Markdown
Member

Needs mcpp 2026.9.11.1, released today, which is what CI now pins.

Three families, and the prefix says which of three questions a member answers

rules-*   how is this translation unit compiled
tools-*   what does the build program need to do itself
dist-*    what comes out of the link, and in what form a user installs it

A dist-* member fits neither of the first two definitions: it compiles no translation unit and does no work while the build program runs. It consumes link outputs through a role = "artifact" action, reached with mcpp pack --format <name>. The prefix matters because the taxonomy is load-bearing — a consumer reading rules-wix would expect a compiler it does not drive and a translation unit, and there is neither.

The engine holds the dispatch and no format. dpkg's control fields, AppImage's runtime, WiX's schema and Apple's notarisation each couple a release to a release mcpp does not control. It is the argument this project already made for languages: Slang is supported without being named in the engine, and a distribution format has less claim to a name there than a language does.

Declare unconditionally, submit conditionally

generate() does both, so a consumer cannot do one without the other. The declaration is what lets the engine answer "which formats does this graph provide" on a build that asked for nothing; the submission must be gated or a plain mcpp build grows an edge it must not have. A member that declared only when asked still works for its author — they always pass their own format — and makes the set unknowable for everyone else.

The members

feature platform notes
dist-appimage Linux xim:appimagetool on the cfg(linux) axis
dist-wix Windows WiX CLI located, not installed — see the correction below
dist-apple macOS base macOS install; codesign only with an identity

dist-appimage is small because the staged tree is already an AppDir bar three files. mcpp pack --mode vendored stages bin/, lib/ and a $ORIGIN-relocatable launcher — which is what an AppImage is. So the member writes an AppRun, a .desktop entry and an icon into it and invokes one tool; it never copies or re-lays-out a directory that can be hundreds of megabytes. The three files are declared as action inputs, which is what makes a change to [package] description reach the graph.

A size floor was its first success check and was wrong on its first real fixture. A stripped hello-world stages at 14999 bytes — under the 16 KB bound — so a correct AppImage was reported as carrying no program. A size is a proxy; the question is answerable directly, so it now counts files the member did not itself write.

dist-wix names the input rather than harvesting a directory, because a bind path that resolved to nothing produced a valid, empty, 52 KB installer with no diagnostic. Its UpgradeCode is derived deterministically from package identity: WiX requires one, a random one breaks upgrades, and a hardcoded one makes every mcpp project the same product.

$(Executable), not $(var.Executable). WiX's documentation spells a -d define the second way; a working implementation green on Windows CI uses the bare form. Neither could be run here, so the one with a measurement behind it ships, and both are recorded.

dist-apple uses four chained actions rather than one, so an icon change does not force a re-copy of the whole staged tree. codesign is off by default — a member that signed unconditionally would fail every build on a machine with no identity in its keychain. iOS is the same shape plus a target row, which is a payload rather than a redesign.

tools-embed gains table()

N inputs, one header, one table, where each row carries the input's key beside its contents and the consumer iterates. file() writes one header per input and files() writes several; neither can express the shape a shader set wants. A duplicate key is refused naming both inputs, and the bytes are a numeric array rather than a raw string literal — a raw literal cannot carry arbitrary binary and its delimiter is terminable by the input, which is the defect in the CMake code this replaces.

The fixture's denominator is the manifest

tests/all-rules-compile names every member a consumer can activate, dist-appimage included. Leaving it out was the alternative and is worse: that fixture exists so every member's module is compiled on every host, and a member left out is one whose macOS and Windows compile is never attempted — exactly what the two platform-specific members are there to catch. Its payload is gated on cfg(linux), so the other two runners download nothing.

CI gains three steps, and two exist because a plan-level assertion is not a measurement

plan_for() returning applies == false on the wrong OS says the gate works and says nothing about whether the tool accepts what the member renders.

  • windows-2022 installs the WiX CLI and builds a real MSI, asserting its size against the linked program's rather than that a file exists — the measured failure is a valid empty installer, and an assertion that stops at existence passes on exactly that artifact.
  • macos-15 builds a real bundle, lints the plist with Apple's own parser, and runs the program through the bundle, because a bundle whose Info.plist names the wrong executable still contains a working program.
  • linux runs mcpp pack --format appimage and executes the result.

A false claim is corrected rather than left to be discovered

dist/wix.cppm said WiX is "not a redistributable archive this ecosystem can vendor". Its own licence file says otherwise: the software is under the Microsoft Reciprocal License, the fee is a maintenance fee on revenue-generating use, and §2 and §3 state that binaries may be redistributed and the source remains freely distributable. The package is anonymously fetchable and immutable from NuGet's flat container (5851349 bytes, HTTP/2 200). So xim:wix is legitimate and this member should declare it with no PATH fallback, the way dist-appimage declares its own tool. The comment now records that as a gap, with the measurement behind it.

Verified locally against mcpp 2026.9.11.1

all-rules-compile      build ok  run ok
embed-consumer         build ok  run ok
embed-module-consumer  build ok  run ok
embed-table-consumer   build ok  run ok: row 0: key=Standard.vert size=20
appimage-consumer      build ok  run ok

pack --format appimage → AppimageConsumer-x86_64.AppImage → "appimage-consumer ok"
host modules compiled: mcpp.dist.appimage, mcpp.dist.apple, mcpp.dist.wix

No emoji in any of the 29 changed files; no deletions against main.

…pe for tools-embed

THREE FAMILIES, AND THE PREFIX SAYS WHICH OF THREE QUESTIONS A MEMBER ANSWERS:

    rules-*   how is this translation unit compiled
    tools-*   what does the build program need to do itself
    dist-*    what comes out of the link, and in what form a user installs it

A `dist-*` member fits neither of the first two definitions. It compiles no
translation unit and does no work while the build program runs: it consumes
LINK OUTPUTS through a `role = "artifact"` action, reached with
`mcpp pack --format <name>` (mcpp 2026.9.11.1+). The prefix matters because the
taxonomy is load-bearing -- a consumer reading `rules-wix` would expect a
compiler it does not drive and a translation unit, and there is neither.

The engine holds the DISPATCH and no format. dpkg's control fields, AppImage's
runtime, WiX's schema and Apple's notarisation each couple a release to a
release mcpp does not control, so none of them is in mcpp. It is the argument
this project already made for languages: Slang is supported without being named
in the engine, and a distribution format has less claim to a name there than a
language does.

DECLARE UNCONDITIONALLY, SUBMIT CONDITIONALLY, and `generate()` does both so a
consumer cannot do one without the other. The declaration is what lets the
engine answer "which formats does this graph provide" on a build that asked for
nothing; the submission must be gated or a plain `mcpp build` grows an edge it
must not have. A member that declared only when asked still works for its
author -- they always pass their own format -- and makes the set unknowable for
everyone else.

`dist-appimage`, Linux. The staged tree is ALREADY an AppDir bar three files,
which is why this member is small: `mcpp pack --mode vendored` stages `bin/`,
`lib/` and a `$ORIGIN`-relocatable launcher, and AppImage additionally wants an
`AppRun`, a top-level `.desktop` entry and an icon. So it writes three small
files into the tree and invokes one tool, and never copies or re-lays-out a
directory that can be hundreds of megabytes. The three are declared as action
inputs, which is what makes a change to `[package] description` reach the
graph. Its payload is `xim:appimagetool`, declared on the `cfg(linux)` axis,
which also carries the type-2 runtime stub carved out of the tool itself --
appimagetool otherwise downloads that stub on every invocation, and a build
must not reach the network.

A SIZE FLOOR WAS THE FIRST VERSION OF ITS SUCCESS CHECK AND WAS WRONG ON ITS
FIRST REAL FIXTURE. A stripped hello-world stages at 14999 bytes, under the
16 KB bound, so a correct AppImage was reported as carrying no program. A size
is a proxy; the question is answerable directly, so it now counts files the
member did not itself write.

`dist-wix`, Windows. Renders a `.wxs` and passes the program in as a
preprocessor variable rather than binding a directory and harvesting it: a bind
path that resolved to nothing produced a valid, empty, 52 KB installer with no
diagnostic, and that measurement is the reason for the shape. The `UpgradeCode`
is derived deterministically from the package identity, because WiX requires
one, a random one breaks upgrades and a hardcoded one makes every mcpp project
the same product. MSI's `ProductVersion` compares only three fields, so the
four-segment date version is repacked and what is lost is stated.

`$(Executable)`, NOT `$(var.Executable)`, AND THE DIFFERENCE IS A MEASUREMENT
RATHER THAN A READING. WiX's documentation spells a `-d` define the second way.
A working implementation that is green on Windows CI uses the bare form; since
neither could be run here, the one with a measurement behind it is the one
shipped, and both are recorded.

`dist-apple`, macOS. A `.app`, its `Info.plist` written directly rather than
through `plutil`, and `codesign` only when an identity is given -- a member that
signed by default would fail every build on a machine with no identity in its
keychain. Four chained actions rather than one, because an icon change must not
force a re-copy of the whole staged tree. iOS is the same shape plus a target
row, which is a payload rather than a redesign.

`tools-embed` GAINS `table()`: N inputs, ONE header, ONE table, where each row
carries the input's key beside its contents and the consumer iterates. `file()`
writes one header per input and `files()` writes several; neither can express
the shape a shader set wants. A duplicate key is refused naming BOTH inputs,
and the bytes are a numeric array rather than a raw string literal -- a raw
literal cannot carry arbitrary binary and its delimiter is terminable by the
input, which is the defect in the CMake code this shape replaces.

THE FIXTURE'S DENOMINATOR IS THE MANIFEST, so `tests/all-rules-compile` names
every member a consumer can activate, `dist-appimage` included. Leaving it out
was the alternative and is worse: that fixture exists so every member's module
is COMPILED on every host, and a member left out is a member whose macOS and
Windows compile is never attempted -- which is exactly what the two
platform-specific members are there to catch. Its payload is gated on
`cfg(linux)`, so the other two runners download nothing.

CI gains three steps, and two of them exist because a plan-level assertion is
not a measurement. `plan_for()` returning `applies == false` on the wrong OS
says the gate works and says nothing about whether the tool accepts what the
member renders. `dist-wix` installs the WiX CLI on windows-2022 and builds a
real MSI, asserting its size against the linked program's rather than that a
file exists -- the measured failure is a valid EMPTY installer, and an
assertion that stops at existence passes on exactly that artifact. `dist-apple`
builds a real bundle on macos-15, lints the plist with Apple's own parser, and
runs the program THROUGH the bundle, because a bundle whose `Info.plist` names
the wrong executable still contains a working program.

A FALSE CLAIM IS CORRECTED IN `dist/wix.cppm` RATHER THAN LEFT TO BE
DISCOVERED. It said WiX is "not a redistributable archive this ecosystem can
vendor". Its own licence file says otherwise: the software is under the
Microsoft Reciprocal License, the fee is a maintenance fee on
revenue-generating use, and sections 2 and 3 state that binaries may be
redistributed and that the source remains freely distributable. The package is
anonymously fetchable and immutable from NuGet's flat container. So `xim:wix`
is legitimate and this member should declare it with no PATH fallback, the way
`dist-appimage` declares its own tool; the comment now records that as a gap
with the measurement behind it.

Verified locally against mcpp 2026.9.11.1: every fixture builds and runs, all
three dist modules compile as host modules, and `mcpp pack --format appimage`
produces an AppImage that executes and prints its program's output.
TWO CI FAILURES, TWO DIFFERENT KINDS.

THE LINUX ONE WAS THE CHECK, NOT THE CODE. It grepped for `struct
embedded_file`, which is the DEFAULT `row_type` -- while the fixture sets
`row_type = "shader_entry"` precisely because that option exists. So the
assertion failed with the code correct, which is what a check tied to a
spelling the fixture chooses will eventually always do. It now reads the
struct's name out of the file and asserts that the table's element type IS
that struct, for every generated header rather than whichever `find` listed
first -- this fixture writes three, and `head -1` was asserting about a file
nothing chose.

THE WINDOWS ONE WAS A TOOL THAT REFUSES TO RUN. An unpinned
`dotnet tool install wix` gets v7:

  wix.exe : error WIX7015: You must accept the Open Source Maintenance Fee
  (OSMF) EULA to use WiX Toolset v7.

A runtime gate, not a redistribution term -- 6's own EULA permits
redistributing binaries under the Microsoft Reciprocal License. So 6.0.2 is
the version this ecosystem can drive, and the version `xim:wix` should carry
when that package lands: one pinning 7 would install a tool that cannot run.

The dispatch itself was already correct when this failed, which is why the fix
is a pin. The same run reported `Distributing msi-consumer v0.3.0 (--format
msi)` and the full `wix.exe build -arch x64 -d Executable=... MsiConsumer.wxs`
argv, so the `.wxs` was rendered and the request reached the member. Only the
tool declined.

The macOS lane stays red for a third, unrelated reason -- `error: cannot
package the Mach-O program 'app-consumer' yet`, which mcpp 2026.9.11.2 fixes
(staging is a service to the provider, not a precondition for dispatch). That
one needs the engine release and a `MCPP_VERSION` bump, not a change here.
The pin worked: WIX7015 is gone, `wix build` completed with no warnings, and
`Packed target/.build-mcpp/out/MsiConsumer-x64.msi` appeared. The check then
refused it -- 32768 bytes against a 114688-byte program, under a
`size > exesize / 2` floor whose comment admitted the ratio was invented ("a
cabinet compresses, so this is a floor and not an equality").

32768 is what a stripped hello-world looks like after a cabinet has had it.
Mostly padding, highly compressible, plus the fixed overhead of an OLE
compound document. The floor was refusing a correct artifact.

THIS IS THE SECOND TIME IN THIS ECOSYSTEM. The AppImage floor was a 16 KB size
bound and refused a correct 14999-byte bundle. Both times the check asked a
proxy question. The AppImage one was replaced by running the AppImage and
asserting the program's output; this one is now an administrative install
(`msiexec /a`), which writes the payload out uncompressed, so the assertion is
EQUALITY against the linked binary rather than a ratio -- and it also proves
the package installs, which a size never did.

The macOS lane stays red on `cannot package the Mach-O program`, which needs
mcpp 2026.9.11.2.
…le reachable

The red macOS lane was the dependency, not a convention. Under 2026.9.11.1:

  error: cannot package the Mach-O program 'app-consumer' yet.

`mcpp pack` staged unconditionally before dispatching and let a staging
failure fail the command, and its built-in closure walk uses
`LD_TRACE_LOADED_OBJECTS` -- which dyld answers by RUNNING the program. So
every dispatched format was unreachable on macOS, including one that reads no
staged tree at all. 2026.9.11.2 makes staging a service to the provider, so
`dist-apple` has been unreachable rather than broken since it was written.

The README floor for `dist-apple` moves with it and says why it is one release
higher than its siblings: the reason is not this member.
…not exist

2026.9.11.2 made the dispatch reachable and the member then refused one layer
up, for the same underlying reason:

  error: no action claimed --format 'app'

That message is the ENGINE's. The member's own explanation -- "mcpp reported no
staged tree" -- went to stderr, which is discarded on a successful build, so
the refusal was unreadable.

And the refusal was wrong. `mcpp pack`'s built-in closure walk refuses a Mach-O
program because it uses `LD_TRACE_LOADED_OBJECTS`, which dyld answers by
RUNNING the program -- so on macOS there is no staged tree to have, and a
member that requires one can never run there. A `.app` needs ONE program, not
a tree: `${mcpp.target_file:<name>}` names it, which is the placeholder
dist/wix.cppm uses for exactly this reason and what section 6 of the design
record recommends for a member packaging a named target.

The tree is still preferred when it exists -- a `--mode vendored` tree carries
the program's dependencies beside it and a bundle should keep them. Without
one the bundle carries the program alone, which is correct for a
self-contained Mach-O and is what the platform's own default produces.

`${mcpp.stage_dir}` is now named only on the path that has a tree, because the
engine REFUSES that placeholder when there is none -- that is its contract,
and a member naming it unconditionally is a member that cannot run on a target
whose built-in staging is refused. `CFBundleExecutable` takes the target's own
name on that path, since a placeholder's basename is not knowable before the
engine expands it.

All eleven features still compile against released 2026.9.11.2.
@Sunrisepeak
Sunrisepeak merged commit 9064107 into main Sep 11, 2026
3 checks passed
@Sunrisepeak
Sunrisepeak deleted the feat/dist-members-and-embed-table branch September 11, 2026 03:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant