Skip to content

Commit ca4cb63

Browse files
0.9.2: dist-apple places deployed files at the bundle's resource destination (mcpp#630) (#22)
* dist-apple: the staged tree's deployed files land at the bundle's resource destination, and the iOS fixture declares its runtime packages (mcpp#630) * README: dist-apple places deployed files at the resource destination * the builtins package is 22.1.8.4 * 0.9.2: dist-apple places deployed files at the resource destination; CI measures under mcpp 2026.9.13.2 * the builtins package is 22.1.8.5 * dist-apple: a step owns its id and description; the version literal is 0.9.2 --------- Co-authored-by: speak-agent <x.d2learn.org@gmail.com>
1 parent 95cbc36 commit ca4cb63

9 files changed

Lines changed: 101 additions & 11 deletions

File tree

.github/workflows/ci.yml

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ env:
2929
# every dispatched format was unreachable on macOS, including one that never
3030
# reads the staged tree. Staging is a service to the provider in 2026.9.11.2,
3131
# and `dist-apple` has been unreachable, not broken, since it was written.
32-
MCPP_VERSION: 2026.9.13.1
32+
MCPP_VERSION: 2026.9.13.2
3333
# PINNED, AND WITHOUT IT THE CACHE BELOW CACHED NOTHING.
3434
#
3535
# A released mcpp is self-contained: with no `MCPP_HOME`, `mcpp self env`
@@ -1805,7 +1805,16 @@ jobs:
18051805
# hand and got subtly wrong would otherwise fail at launch time on a
18061806
# user's machine and nowhere here.
18071807
plutil -lint "$app/Contents/Info.plist"
1808-
echo "ok: one bundle, a valid plist, and it launches"
1808+
# THE DEPLOYED FILE, AT THE RESOURCE DESTINATION AND NOT BESIDE THE
1809+
# PROGRAM (mcpp#630, item 3a): the engine stages `bin/data/...` and
1810+
# this member places it under `Contents/Resources/`.
1811+
test -f "$app/Contents/Resources/data/greeting.txt" \
1812+
|| { echo "FAIL: the deployed file is not under Contents/Resources"; find "$app" -type f; exit 1; }
1813+
if [ -e "$app/Contents/MacOS/bin" ]; then
1814+
echo "FAIL: the staged tree was copied whole into Contents/MacOS"; find "$app" -type f; exit 1
1815+
fi
1816+
grep -q 'closure = not-walked' pack.log || echo "note: the stage manifest's closure line was not echoed by pack"
1817+
echo "ok: one bundle, a valid plist, it launches, and the resource is where NSBundle looks"
18091818
18101819
# THE iOS ROW, FOR REAL (#622 B1's other half). `tests/ios-app-consumer`
18111820
# is the same fixture the Linux `consumers` job checks at the plan
@@ -1868,6 +1877,12 @@ jobs:
18681877
[ "$plat" = "iPhoneSimulator" ] || {
18691878
echo "FAIL: CFBundleSupportedPlatforms is '$plat', expected iPhoneSimulator"
18701879
exit 1; }
1880+
# THE DEPLOYED FILE, AT THE BUNDLE ROOT (the flat layout), and the
1881+
# executable at the root by its bare name -- not under `bin/`.
1882+
test -f "$app/data/greeting.txt" \
1883+
|| { echo "FAIL: the deployed file is not at the bundle root"; find "$app" -type f; exit 1; }
1884+
test -x "$app/ios-app-consumer" \
1885+
|| { echo "FAIL: the executable is not at the bundle root"; find "$app" -type f; exit 1; }
18711886
18721887
exe=$(/usr/libexec/PlistBuddy -c 'Print :CFBundleExecutable' "$plist")
18731888
test -x "$app/$exe" \

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ engine's own module family and is not used here.
7373
| `tools-island` | `mcpp.tools.island` | 2026.9.7.1 | nothing beyond mcpp: it reads marked entry points out of an island's own source and writes the `extern "C"` boundary header its compiler reads and the module the C++ side imports. Not a device rule -- it claims no extension, and a project calls it from its own `build.mcpp` |
7474
| `dist-appimage` | `mcpp.dist.appimage` | 2026.9.11.1 | `xim:appimagetool`, which this feature declares on the `cfg(linux)` axis. Linux only. Turns the tree `mcpp pack` staged into one AppImage: the staged bundle is already an AppDir bar three files, 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 tree that can be hundreds of megabytes |
7575
| `dist-wix` | `mcpp.dist.wix` | 2026.9.11.1 | `xim:wix`, which this feature declares on the Windows target axis; the .NET 6 runtime the tool needs is a Windows component the payload does not carry, and `wix --version` names it when it is missing. Windows only. Renders a `.wxs` and passes the program in as a preprocessor variable, because a bind path that resolves to nothing is silent |
76-
| `dist-apple` | `mcpp.dist.apple` | 2026.9.11.2 (macOS), 2026.9.12.3 (iOS) | the base macOS install (`ditto`, and `codesign` only when an identity is given). macOS: `Contents/`-shaped, as always. iOS (`aarch64-ios-sim`, `aarch64-ios`): a flat bundle at the same call site -- no separate feature, no separate module -- with `MinimumOSVersion` from `mcpp::min_platform_version()` (#622 A11), `CFBundleSupportedPlatforms` read from `env == "sim"`, `UIDeviceFamily`, `LSRequiresIPhoneOS`, and a directory of flat PNGs listed under `CFBundleIcons` in place of macOS's single `.icns` file. Signing is skipped on the simulator row (`options::identity` is ignored, with a `mcpp::warning` naming why) and unchanged on the device row. The iOS row is measured end to end on `macos-15`: a real `mcpp build`, `mcpp pack --format app` and `mcpp run` against `aarch64-ios-sim`, through `xim:apple-simulator-tools`' `simctl-run`. **The macOS floor is one release higher than its siblings** and the reason is not this member: under 2026.9.11.1 `mcpp pack` staged before dispatching and let a staging failure fail the command, so on a Mach-O program -- which the built-in closure walk refuses, because it uses `LD_TRACE_LOADED_OBJECTS` and dyld answers that by running the program -- every dispatched format was unreachable, including one that reads no staged tree. 2026.9.11.2 makes staging a service to the provider |
76+
| `dist-apple` | `mcpp.dist.apple` | 2026.9.11.2 (macOS), 2026.9.12.3 (iOS) | the base macOS install (`ditto`, and `codesign` only when an identity is given). macOS: `Contents/`-shaped, as always. iOS (`aarch64-ios-sim`, `aarch64-ios`): a flat bundle at the same call site -- no separate feature, no separate module -- with `MinimumOSVersion` from `mcpp::min_platform_version()` (#622 A11), `CFBundleSupportedPlatforms` read from `env == "sim"`, `UIDeviceFamily`, `LSRequiresIPhoneOS`, and a directory of flat PNGs listed under `CFBundleIcons` in place of macOS's single `.icns` file. Signing is skipped on the simulator row (`options::identity` is ignored, with a `mcpp::warning` naming why) and unchanged on the device row. The iOS row is measured end to end on `macos-15`: a real `mcpp build`, `mcpp pack --format app` and `mcpp run` against `aarch64-ios-sim`, through `xim:apple-simulator-tools`' `simctl-run`. **The macOS floor is one release higher than its siblings** and the reason is not this member: under 2026.9.11.1 `mcpp pack` staged before dispatching and let a staging failure fail the command, so on a Mach-O program -- which the built-in closure walk refuses, because it uses `LD_TRACE_LOADED_OBJECTS` and dyld answers that by running the program -- every dispatched format was unreachable, including one that reads no staged tree. 2026.9.11.2 makes staging a service to the provider. From 0.9.2 the staged tree's deployed files (`bin/<to>/...`, which the engine stages for a Mach-O program before the closure walk since the release for mcpp#630) land at the bundle's resource destination -- `Contents/Resources/<to>/...` on macOS, the bundle root on iOS -- and the launcher alone goes to the executable directory, so `CFBundleExecutable` names a file that is where it says. The iOS fixture declares `llvm.libcxx` and `llvm.compiler-rt-builtins` under `cfg(os = "ios")`, which is what an application that imports `std` on those rows declares |
7777
| `dist-web` | `mcpp.dist.web` | 2026.9.13.1, the release that carries `${mcpp.self}` and `mcpp stage`'s argument shape as an engine contract (`stage --verify content --output <dst> <src>`) -- what lets this member's copy run on every host mcpp does, Windows included, in place of the `cp` this member used through 0.8.0 | nothing beyond mcpp: `wasm32-emscripten` only. Copies `${mcpp.stage_dir}/bin/` -- the `.js` launcher, the implicit `.wasm`, the `.data` when present, and every `mcpp::deploy`'d file, all of which #622 A5 and A4 already stage there -- to `<out_dir>/web/`, dropping the `bin/` prefix a browser has no use for, and writes an `index.html` rendered from a project template or a built-in default that loads the script with a plain `<script src>`. Each staged file and the rendered page are copied with `${mcpp.self} stage --verify content --output <dst> <src>`, the same copier every `stage_file` edge in `build.ninja` already runs -- no host-specific copy tool, no plan-time `create_directories` (`stage` creates the destination's parent), and a second `mcpp pack --format web` with nothing changed copies nothing |
7878
| `dist-apk` | `mcpp.dist.apk` | 2026.9.13.1, raised alongside `dist-web` in the same 0.9.0 release: this member's own manifest-template and Java-array changes ask nothing new of the engine, but this collection publishes one package at one version, and this is the release CI verifies it under from here on | `xim:android-build-tools`, `xim:android-platform` (versioned by API level, read back for `targetSdkVersion`), `xim:jdk-temurin` (`javac`/`jar`; `android-build-tools`' own runtime dependency provisions a JDK for its OWN wrappers only), `xim:android-debug-keystore`, all on the `cfg(env = "android")` axis. Generates `AndroidManifest.xml` and signs with the published Android debug key by default. Level 0 needs no Java (`hasCode="false"`, `android.app.NativeActivity`); `options::java_sources` adds `javac` + `d8` and a real `<activity>`. `options::manifest_template` renders a project manifest with six tokens substituted verbatim; `{{application_id}}` and `{{activity}}` are required always and `{{lib_name}}` at level 0, each refused by name at plan time when missing (naming `assets/mcpp-run.json`, which `adb-run` reads them from too) or when the template names an unknown token; empty renders 0.8.0's manifest byte-identically. `options::resources` is a project's own `res/`, linked as the application's base resources from 0.9.1 (0.9.0 linked it as an aapt2 overlay, which refuses every resource the base does not already define -- a launcher icon could not be supplied). `options::java_sources` is an array: one `javac` over every root's `.java` files and one `d8` over the result, so a project's own sources and a path dependency's join without being merged into one directory first, and `rerun_if_changed_glob` is declared only for a root under `mcpp::manifest_dir()` -- a dependency root's files are already inputs of the `javac` action and its version is already in the build's fingerprint. Android only -- an `app` target is a shared object on this row (#622 A3), and this member reads its native library from the staged tree and whatever `mcpp::deploy` placed beside the ordinary build's link output, because Android's own closure stages neither the dependency set nor the deployed files (see `dist/apk.cppm`'s header). CI packages both level 0 and level 1 and checks the archive (`libc++_shared.so` bundled only when the closure's `NEEDED` entries require it, `mcpp::deploy`'d files under `assets/`); the runner has no emulator or device, so the two rows that actually run were measured locally on 2026-09-12, through `adb-run`, against a KVM-accelerated x86_64 emulator and a physical arm64-v8a phone, both printing `1-2-3` and exiting 0 |
7979

dist/apple.cppm

Lines changed: 59 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -246,9 +246,12 @@ struct options {
246246
// a different signing flag -- from becoming a reimplementation of this
247247
// member, the same trade `dist/appimage.cppm` makes.
248248
struct step {
249-
const char* id;
250-
const char* role;
251-
const char* description;
249+
// Owned strings rather than literals: the resource steps below name one
250+
// action per deployed entry, and an id derived from a file name has to
251+
// outlive the function that formed it.
252+
std::string id;
253+
const char* role;
254+
std::string description;
252255
std::vector<std::string> argv;
253256
std::vector<std::string> inputs;
254257
// MORE THAN ONE OUTPUT ON THE iOS ICON STEP: a directory of PNGs copies
@@ -713,16 +716,66 @@ inline plan plan_for(options opt = {}) {
713716
// one and the program when there is not, and the `inputs` entry is the
714717
// same either way: the program is what this bundle is FOR, and naming it
715718
// is what orders this action after the link.
719+
//
720+
// WITH A STAGED TREE IT COPIES THE PROGRAM, NOT THE TREE. The engine
721+
// stages a Mach-O program as `bin/<name>` with every deployed file under
722+
// `bin/<to>/...` beside it (mcpp#630, item 3a: staging precedes the
723+
// closure walk, and the tree is handed over with `closure = not-walked`
724+
// in its manifest). Copying that tree whole into `Contents/MacOS/` put
725+
// the executable at `Contents/MacOS/bin/<name>`, which is not where
726+
// `CFBundleExecutable` says it is, and the resources beside a program
727+
// rather than in `Contents/Resources/`, where `NSBundle` looks. So the
728+
// launcher goes to the executable directory by itself, and the staged
729+
// `bin/` entries that are not the launcher go to the bundle's resource
730+
// destination below, at the relative path `mcpp::deploy`'s `to` gave
731+
// them. `${mcpp.stage_dir}` is still named as an input of the launcher
732+
// copy, which is what orders every step here after staging and gives
733+
// the action its dependency on the tree's manifest.
716734
layout.argv = stage.empty()
717735
? std::vector<std::string>{ "ditto",
718736
std::format("${{mcpp.target_file:{}}}", target),
719737
execDir + "/" + executableName }
720-
: std::vector<std::string>{ "ditto", "${mcpp.stage_dir}", execDir };
738+
: std::vector<std::string>{ "ditto", launcher, execDir + "/" + executableName };
721739
layout.inputs = { std::format("${{mcpp.target_file:{}}}", target) };
740+
if (!stage.empty()) layout.inputs.push_back("${mcpp.stage_dir}");
722741
layout.outputs = { execDir + "/" + executableName };
723742
p.steps.push_back(layout);
724743
assembled.push_back(execDir + "/" + executableName);
725744

745+
// THE DEPLOYED FILES, AT THE BUNDLE'S RESOURCE DESTINATION.
746+
//
747+
// Read at plan time against the staged tree, the way `dist-apk` reads
748+
// its assets: the second pass of `mcpp pack --format app` runs this
749+
// program after the tree is staged, so the entries exist on disk here.
750+
// One action per top-level entry under `bin/` that is not the launcher,
751+
// a file or a whole directory, each named by its own path so that the
752+
// graph carries an edge per deployed thing rather than one edge for
753+
// "everything". `ditto <dir> <dir>` copies the source's CONTENTS into
754+
// the destination, which is the flat layout iOS wants at the bundle
755+
// root and the `Contents/Resources/<to>/` layout macOS wants.
756+
if (!stage.empty()) {
757+
const std::filesystem::path stageBin = std::filesystem::path(stage) / "bin";
758+
const std::filesystem::path launcherPath = std::filesystem::path(launcher);
759+
std::error_code ec;
760+
std::vector<std::filesystem::path> entries;
761+
if (std::filesystem::is_directory(stageBin, ec))
762+
for (auto const& e : std::filesystem::directory_iterator(stageBin, ec))
763+
if (!ec && e.path() != launcherPath) entries.push_back(e.path());
764+
std::ranges::sort(entries);
765+
for (auto const& e : entries) {
766+
const std::string rel = e.filename().string();
767+
step res;
768+
res.id = "mcpp.dist.apple.resource." + rel;
769+
res.role = "artifact";
770+
res.description = "APP RESOURCE " + rel;
771+
res.argv = { "ditto", e.string(), resourceDir + "/" + rel };
772+
res.inputs = { e.string(), "${mcpp.stage_dir}" };
773+
res.outputs = { resourceDir + "/" + rel };
774+
p.steps.push_back(res);
775+
assembled.push_back(resourceDir + "/" + rel);
776+
}
777+
}
778+
726779
if (!opt.icon.empty()) {
727780
step icon;
728781
icon.id = "mcpp.dist.apple.icon";
@@ -827,9 +880,9 @@ inline bool submit(const plan& p) {
827880
if (!p.applies) return true;
828881
for (auto const& s : p.steps) {
829882
mcpp::action a;
830-
a.id = s.id;
883+
a.id = s.id.c_str();
831884
a.role = s.role;
832-
a.description = s.description;
885+
a.description = s.description.c_str();
833886
for (auto const& tok : s.argv) a.arg(tok.c_str());
834887
for (auto const& in : s.inputs) a.input(in.c_str());
835888
for (auto const& out : s.outputs) a.output(out.c_str());

mcpp.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
[package]
22
name = "plugins"
33
namespace = "mcpp"
4-
version = "0.9.1"
4+
version = "0.9.2"
55
description = "Official mcpp build plugins: rule packages under mcpp.rules.*, build-time utilities under mcpp.tools.*, each member selected by a feature"
66
license = "Apache-2.0"
77
authors = ["mcpp-community"]

src/plugins.cppm

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ export namespace mcpp::plugins {
4949
//
5050
// One package, one version: the number lives in mcpp.toml, and the CI step
5151
// `the collection states its own version` compares the two.
52-
inline constexpr std::string_view version = "0.9.1";
52+
inline constexpr std::string_view version = "0.9.2";
5353

5454
} // namespace mcpp::plugins
5555

tests/app-consumer/mcpp.toml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,3 +23,10 @@ plugins = { path = "../..", features = ["dist-apple"], host-module = true }
2323
[targets.app-consumer]
2424
kind = "bin"
2525
main = "src/main.cpp"
26+
27+
# A deployed file, which the bundle must carry at its resource destination
28+
# (`Contents/Resources/data/greeting.txt` on macOS). The engine stages it at
29+
# `bin/data/greeting.txt` beside the program (mcpp#630, item 3a), and
30+
# `dist-apple` places every such entry under the resource directory.
31+
[runtime]
32+
deploy = [ { from = "share/greeting.txt", to = "data" } ]
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
bundled resource

tests/ios-app-consumer/mcpp.toml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,19 @@ ios_deployment_target = "17.0"
5959
kind = "bin"
6060
main = "src/main.cpp"
6161

62+
# THE C++ STANDARD LIBRARY AND THE COMPILER RUNTIME ARE PACKAGES ON THE iOS
63+
# ROWS (mcpp#630, item 4). The payload's static libc++ is a macOS object and
64+
# its resource directory carries no iOS builtins archive; without these two
65+
# lines the engine links the SDK's libc++ under the payload's headers and
66+
# reports the pairing once. This fixture imports `std`, so it declares them.
67+
[target.'cfg(os = "ios")'.dependencies]
68+
llvm.libcxx = "22.1.8.1"
69+
llvm.compiler-rt-builtins = "22.1.8.5"
70+
71+
# A deployed file, which the iOS bundle carries at its root (the flat layout).
72+
[runtime]
73+
deploy = [ { from = "share/greeting.txt", to = "data" } ]
74+
6275
# THE SIMULATOR ROW ONLY -- the device row (`aarch64-ios`) keeps `runner`
6376
# unset, as `examples/13-platform-targets/mcpp.toml` does: an artifact cannot
6477
# be run off an iOS device without a signature the developer owns.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
bundled resource

0 commit comments

Comments
 (0)