Skip to content

Commit dea1f09

Browse files
authored
0.12.0: omit_keys, a named web page, the engine's strip decision, rules-swift, and contributions from the resolved graph (#28)
* dist-web: options::page names the page (#649 P2) * dist-apple: options::omit_keys leaves a defaulted Info.plist key out (#649 P1) * dist-apk follows the engine's strip decision: --no-strip and --debug-symbols reach the packed libraries (#649 E5) * rules-swift: one package's Swift sources compile into its images, with a generated header and the Swift runtime on the link (#647 E2) * 0.12.0: the README states omit_keys, page, the engine's strip decision in dist-apk, and rules-swift * dist-apk and dist-apple collect what the resolved graph's packages contribute; one JSON reader for both (#647 E1) * dist-apk packs the libraries the engine staged as staged, and says so when keep_debug_symbols cannot restore them (#649 E5) * rules-swift: the module's compilation declares its dependency file The repository requires every rule to pass one: swiftc writes the same Makefile-style file clang does, and without it a bridged header could change with no rebuild. * Two contributors naming one file: the closer one decides it, and one destination has one claimant Review of the 0.12.0 branch before merge found the graph-contribution merge inconsistent with the precedence its own documentation states. `contributions` is ordered highest priority first: the application's own libraries, then the graph's packages requesters first. The resources merge walks it backwards, so a higher-priority contributor's resource overrides a lower one's through `aapt2 -R`. The assets merge walked it forwards into `collect_tree`, which copies with `overwrite_existing`, so the LAST write won and the deepest dependency decided a file two packages name. Measured on the fixture below: `assets/graph-asset.txt` held `from-the-deeper-library` where the documented order gives `from-the-requester`. The walk is now backwards, for the same reason the resources walk is. `lib/<abi>/` is flat, so the application's own library, an archive's native library of the same name, and a second archive's copy of it all address one file. Each went through `place_library`, which emits a step whose id and output are derived from the ABI and the leaf name: two claimants produced two steps with one id writing one path. One destination now has one claimant, the first in the priority order, and a later claim is reported by name rather than written. `mcpp::plugins::json` decoded each `\u` escape on its own, so a code point above U+FFFF -- which reaches JSON as a surrogate PAIR -- became two three-byte sequences holding unpaired surrogates: not UTF-8, and silently wrong in whatever `AndroidManifest.xml` or `Info.plist` the value was written into. Pairs are now combined, and a surrogate that is not half of one is refused. mcpp's own writer escapes only characters below 0x20 and passes UTF-8 through, so no producer reaches this path today, which is why nothing reported it; the reader is shared by `dist-apk` and `dist-apple` and should not corrupt what it cannot represent. Criterion: `tests/apk-consumer-graph` gains `lib2/`, which `lib/` depends on, so the two contribute one asset name at two distances. Leg (6) reads the packed `assets/graph-asset.txt`. With the walk reverted it fails with `from-the-deeper-library`; with it in place the five legs pass on 2026.9.16.1. * CI builds against the released 2026.9.16.1, so the strip-decision legs run `MCPP_VERSION` moves 2026.9.14.2 -> 2026.9.16.1, the release this collection's 0.12.0 members are written against. The pin is what decides whether a leg runs or reports itself skipped. `tests/apk-consumer` legs (m) and (n) -- `mcpp pack --no-strip` and `--debug-symbols <dir>` reaching the packed libraries -- are gated inside the script on an engine that publishes `MCPP_PACK_STRIP`, so under the old pin they printed `skip: (m),(n) need an engine that publishes MCPP_PACK_STRIP` and the member's central 0.12.0 change had no CI reading at all. It has one now. The step's own comment carried a deferral that had already retired: it said the Android packaging step was not yet green under the pinned version because `kind = "app"` and `mcpp::min_platform_version()` are #622 engine additions. The pin has been above that release since 2026.9.14.2. The note now states what is true, and what the new pin adds.
1 parent d6bee6a commit dea1f09

36 files changed

Lines changed: 1811 additions & 165 deletions

File tree

.github/workflows/ci.yml

Lines changed: 32 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ env:
4444
# through the runner named after its format, which is how `mcpp run --format
4545
# app` reaches `macapp-run`. An engine below it stages no `needs` lines, and
4646
# `dist-apk` refuses its tree naming this release.
47-
MCPP_VERSION: 2026.9.14.2
47+
MCPP_VERSION: 2026.9.16.1
4848
# AN ENGINE BUILT FROM SOURCE, WHEN A DISPATCH NAMES ONE.
4949
#
5050
# Empty on every push and pull request, so the steps run the release above.
@@ -1249,11 +1249,12 @@ jobs:
12491249
# does; `dist-apk` provisions the four payloads on its own
12501250
# `feature-xlings.dist-apk` table (`xim:android-build-tools`, `xim:
12511251
# android-platform`, `xim:jdk-temurin`, `xim:android-debug-keystore`).
1252-
# NOT YET GREEN under this job's pinned `MCPP_VERSION`: `kind = "app"`
1253-
# and `mcpp::min_platform_version()` are #622 engine additions, and
1254-
# this step needs the release that carries them (see this repository's
1255-
# README, `dist-apk`'s own row) -- raising the pin is P5's own change,
1256-
# together with the rest of this collection's release.
1252+
# `kind = "app"` and `mcpp::min_platform_version()` are #622 engine
1253+
# additions, and this step needed the release that carries them; the pin
1254+
# has been above it since 2026.9.14.2 and the step is green. From
1255+
# 2026.9.16.1 the pin also carries the engine's strip decision
1256+
# (`MCPP_PACK_STRIP`, `MCPP_PACK_DEBUG_SYMBOLS_DIR`), which is what lets
1257+
# legs (m) and (n) below run rather than report themselves skipped.
12571258
#
12581259
# THIS STEP ASSERTS PACKAGING ONLY, DELIBERATELY, NOT `mcpp run
12591260
# --format apk`. This fixture's own `main.cpp` and `mcpp.toml` DO wire
@@ -1352,6 +1353,19 @@ jobs:
13521353
working-directory: tests/apk-consumer-libraries
13531354
run: MCPP="$MCPP" ./check-apk-libraries.sh
13541355

1356+
# LIBRARIES THE GRAPH CONTRIBUTES (0.12.0). `tests/apk-consumer-graph/lib`
1357+
# states `[package.metadata.dist-apk]`, and the application names it only
1358+
# as a dependency. Under an engine that gives the root build program the
1359+
# resolved graph (mcpp 2026.9.16.1+) the library's resource is in the APK,
1360+
# `graph_libraries = false` reads none, the application's own library wins
1361+
# a resource both define, and a malformed contribution is refused naming
1362+
# the package and the key; under an older engine the pack succeeds without
1363+
# the contribution. The script chooses its legs from `mcpp --version`.
1364+
- name: dist-apk collects the libraries the resolved graph contributes
1365+
timeout-minutes: 30
1366+
working-directory: tests/apk-consumer-graph
1367+
run: MCPP="$MCPP" ./check-apk-graph.sh
1368+
13551369
# Compiles the device unit on a machine with no GPU: the clang route
13561370
# produces sm_89 code from the payload toolkit. Running it needs a
13571371
# device, so the run is of the CPU variant, which the same seam serves.
@@ -2046,6 +2060,18 @@ jobs:
20462060
working-directory: tests/metal-consumer
20472061
run: MCPP="$MCPP" ./check-metal.sh
20482062

2063+
# rules-swift (mcpp#647 E2): a C++ program calling a Swift function, which
2064+
# calls back into C through the bridging header, printing a line from each
2065+
# side; the generated header declares the exported function; the fixture
2066+
# links for the iOS simulator when the runner has that SDK. On a runner
2067+
# without swiftc the script asserts the rule's refusal and annotates the
2068+
# run instead.
2069+
- name: rules-swift compiles a package's Swift sources, or records that this runner has no swiftc
2070+
if: ${{ !cancelled() && runner.os == 'macOS' }}
2071+
timeout-minutes: 20
2072+
working-directory: tests/swift-consumer
2073+
run: MCPP="$MCPP" ./check-swift.sh
2074+
20492075
# THE iOS ROW, FOR REAL (#622 B1's other half). `tests/ios-app-consumer`
20502076
# is the same fixture the Linux `consumers` job checks at the plan
20512077
# level (see that fixture's own header); here it is built, packed and

README.md

Lines changed: 63 additions & 4 deletions
Large diffs are not rendered by default.

dist/apk.cppm

Lines changed: 236 additions & 129 deletions
Large diffs are not rendered by default.

dist/apple.cppm

Lines changed: 149 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -293,6 +293,35 @@ struct options {
293293
// `NSHighResolutionCapable`) are replaced by the project's value.
294294
std::string info_plist;
295295

296+
// DEFAULTED Info.plist KEYS TO LEAVE OUT (0.12.0). A property list has no
297+
// null, so `info_plist` can replace a default's value but cannot remove the
298+
// key; a project whose other build states neither `NSHighResolutionCapable`
299+
// nor `LSRequiresIPhoneOS` names them here (#649 P1). Only the three keys
300+
// this member defaults are accepted: a key it derives is refused by name, as
301+
// `info_plist` refuses it, and so is any other key, which this member never
302+
// writes. A key named here and also set by `info_plist` is refused, because
303+
// the two statements contradict each other. A key that does not apply to the
304+
// row (`UIDeviceFamily` on macOS) is accepted and changes nothing.
305+
std::vector<std::string> omit_keys;
306+
307+
// Info.plist ENTRIES THE GRAPH CONTRIBUTES (0.12.0). With mcpp 2026.9.16.1
308+
// the root project's build program receives the resolved graph, and every
309+
// package in it other than the application that states
310+
// `[package.metadata.dist-apple] info_plist = "<file>"` contributes that
311+
// plist's entries, the path relative to the package's directory: a library
312+
// that needs a usage description or a background mode states it once, and
313+
// every application that depends on it carries it. The entries are applied
314+
// in the graph's order, dependencies first, so a package overrides the
315+
// packages it depends on, and the application's own `info_plist` is applied
316+
// last and wins every key. The keys this member derives are refused in a
317+
// contribution as in `info_plist`, naming the package; a key the application
318+
// names in `omit_keys` is left out whoever contributes it.
319+
//
320+
// `false` reads no contribution. Under an older engine, or in a dependency's
321+
// own build program, there is no graph, and nothing is contributed either
322+
// way.
323+
bool graph_info_plist = true;
324+
296325
// AN iOS DEVICE BUNDLE'S PROVISIONING PROFILE (0.11.0), manifest-relative or
297326
// absolute: embedded as `embedded.mobileprovision`, and, when `entitlements`
298327
// is empty, the source of the entitlements the bundle is signed with (the
@@ -662,44 +691,52 @@ inline bool dict_entries(const mcpp::plugins::xml::node& dict,
662691
return true;
663692
}
664693

665-
// Reads `options::info_plist`: the entries to add, as the plist text
666-
// `plist_document` inserts, and the defaulted keys they replace.
667-
inline bool read_info_plist_fragment(const std::string& path, std::string& entries,
668-
std::vector<std::string>& replaced, std::string& message) {
694+
// One Info.plist entry a fragment states: its key and the plist text of the
695+
// `<key>` and its value, as `plist_document` inserts it.
696+
struct plist_entry {
697+
std::string key;
698+
std::string text;
699+
};
700+
701+
// Reads one plist fragment's entries. `who` is how a diagnostic names the
702+
// fragment (`options::info_plist`, or a package's [package.metadata.dist-apple]).
703+
inline bool read_plist_entries(const std::string& path, const std::string& who,
704+
std::vector<plist_entry>& out, std::string& message) {
669705
namespace xml = mcpp::plugins::xml;
670706
xml::node root;
671707
std::string err;
672708
if (!xml::parse(read_text(path), root, err)) {
673-
message = std::format("mcpp.dist.apple: `options::info_plist` ({}) cannot be read: {}", path, err);
709+
message = std::format("mcpp.dist.apple: {} ({}) cannot be read: {}", who, path, err);
674710
return false;
675711
}
676712
const xml::node* dict = top_dict(root);
677713
if (!dict) {
678-
message = std::format("mcpp.dist.apple: `options::info_plist` ({}) has no top-level <dict>.", path);
714+
message = std::format("mcpp.dist.apple: {} ({}) has no top-level <dict>.", who, path);
679715
return false;
680716
}
681717
std::vector<std::pair<std::string, const xml::node*>> pairs;
682718
if (!dict_entries(*dict, pairs, err)) {
683-
message = std::format("mcpp.dist.apple: `options::info_plist` ({}): {}", path, err);
719+
message = std::format("mcpp.dist.apple: {} ({}): {}", who, path, err);
684720
return false;
685721
}
686722
std::vector<std::string> seen;
687723
for (auto const& [key, value] : pairs) {
688724
if (std::ranges::find(derived_plist_keys(), key) != derived_plist_keys().end()) {
689725
message = std::format(
690-
"mcpp.dist.apple: `options::info_plist` ({}) sets {}, which this member derives "
691-
"from its options and the engine; set the option instead.", path, key);
726+
"mcpp.dist.apple: {} ({}) sets {}, which this member derives "
727+
"from its options and the engine; set the option instead.", who, path, key);
692728
return false;
693729
}
694730
if (std::ranges::find(seen, key) != seen.end()) {
695-
message = std::format("mcpp.dist.apple: `options::info_plist` ({}) sets {} twice.", path, key);
731+
message = std::format("mcpp.dist.apple: {} ({}) sets {} twice.", who, path, key);
696732
return false;
697733
}
698734
seen.push_back(key);
699-
if (std::ranges::find(defaulted_plist_keys(), key) != defaulted_plist_keys().end())
700-
replaced.push_back(key);
701-
entries += " <key>" + key + "</key>\n";
702-
xml::write(*value, entries, 1);
735+
plist_entry e;
736+
e.key = key;
737+
e.text = " <key>" + key + "</key>\n";
738+
xml::write(*value, e.text, 1);
739+
out.push_back(std::move(e));
703740
}
704741
return true;
705742
}
@@ -938,16 +975,113 @@ inline plan plan_for(options opt = {}) {
938975
opt.provisioning_profile = resolve_path(opt.provisioning_profile);
939976
std::string extraEntries;
940977
std::vector<std::string> replacedKeys;
978+
std::vector<std::string> contributedDefaults;
979+
// The application's own entries, read first so that their refusals come
980+
// before any contribution's; they are applied last.
981+
std::vector<plist_entry> ownEntries;
941982
if (!opt.info_plist.empty()) {
942983
if (!is_file(opt.info_plist)) {
943984
return refuse(p, "info_plist not found", std::format(
944985
"mcpp.dist.apple: `options::info_plist` ({}) was not found", opt.info_plist));
945986
}
946987
mcpp::rerun_if_changed(opt.info_plist.c_str());
947988
std::string message;
948-
if (!read_info_plist_fragment(opt.info_plist, extraEntries, replacedKeys, message))
989+
if (!read_plist_entries(opt.info_plist, "`options::info_plist`", ownEntries, message))
949990
return refuse(p, "unusable info_plist", message);
991+
for (auto const& e : ownEntries)
992+
if (std::ranges::find(defaulted_plist_keys(), e.key) != defaulted_plist_keys().end())
993+
replacedKeys.push_back(e.key);
994+
}
995+
996+
// The entries the graph's packages contribute, in the graph's order
997+
// (dependencies first), each replacing an earlier contribution of the same
998+
// key; then the application's own, replacing any contribution.
999+
std::vector<plist_entry> merged;
1000+
const auto apply_entries = [&](const std::vector<plist_entry>& list) {
1001+
for (auto const& e : list) {
1002+
auto it = std::ranges::find_if(merged, [&](const plist_entry& m) { return m.key == e.key; });
1003+
if (it != merged.end()) it->text = e.text;
1004+
else merged.push_back(e);
1005+
}
1006+
};
1007+
if (opt.graph_info_plist) {
1008+
auto graph = mcpp::plugins::graph::read();
1009+
if (!graph) {
1010+
return refuse(p, "unreadable graph document", std::format(
1011+
"mcpp.dist.apple: {}; the Info.plist entries its packages contribute cannot be "
1012+
"collected. Set `options::graph_info_plist = false` to read none.", graph.error()));
1013+
}
1014+
for (auto const& pkg : graph->packages) {
1015+
if (pkg.root) continue;
1016+
const auto* t = mcpp::plugins::graph::table_of(pkg, "dist-apple");
1017+
if (!t) continue;
1018+
const std::string who = std::format("the [package.metadata.dist-apple] of {}",
1019+
mcpp::plugins::graph::label_of(pkg));
1020+
for (auto const& [key, v] : t->members) {
1021+
if (key == "info_plist") continue;
1022+
mcpp::warning(std::format(
1023+
"mcpp.dist.apple: {} states `{}`, which this version of the member does not "
1024+
"read; it is ignored.", who, key).c_str());
1025+
}
1026+
const auto* ip = t->get("info_plist");
1027+
if (!ip) continue;
1028+
if (ip->type != mcpp::plugins::json::value::kind::string || ip->text.empty()) {
1029+
return refuse(p, "malformed graph contribution", std::format(
1030+
"mcpp.dist.apple: {} states `info_plist` as something other than a path.", who));
1031+
}
1032+
const std::string path = mcpp::plugins::graph::resolve(pkg, ip->text);
1033+
if (!is_file(path)) {
1034+
return refuse(p, "contributed info_plist not found", std::format(
1035+
"mcpp.dist.apple: {} names the Info.plist fragment {}, which was not found.", who, path));
1036+
}
1037+
mcpp::rerun_if_changed(path.c_str());
1038+
std::vector<plist_entry> list;
1039+
std::string message;
1040+
if (!read_plist_entries(path, who, list, message))
1041+
return refuse(p, "unusable contributed info_plist", message);
1042+
apply_entries(list);
1043+
}
1044+
}
1045+
// The application's own entries win; a key it omits is left out whoever
1046+
// contributed it. The omit_keys checks below still see only the
1047+
// application's own entries in `replacedKeys`.
1048+
apply_entries(ownEntries);
1049+
for (auto const& e : merged) {
1050+
if (std::ranges::find(opt.omit_keys, e.key) != opt.omit_keys.end()) continue;
1051+
extraEntries += e.text;
1052+
if (std::ranges::find(defaulted_plist_keys(), e.key) != defaulted_plist_keys().end()
1053+
&& std::ranges::find(replacedKeys, e.key) == replacedKeys.end())
1054+
contributedDefaults.push_back(e.key);
1055+
}
1056+
for (std::size_t i = 0; i < opt.omit_keys.size(); ++i) {
1057+
const auto& key = opt.omit_keys[i];
1058+
if (std::ranges::find(derived_plist_keys(), key) != derived_plist_keys().end()) {
1059+
return refuse(p, "omit_keys names a derived key", std::format(
1060+
"mcpp.dist.apple: `options::omit_keys` names {}, which this member derives "
1061+
"from its options and the engine; a derived key cannot be omitted.", key));
1062+
}
1063+
if (std::ranges::find(defaulted_plist_keys(), key) == defaulted_plist_keys().end()) {
1064+
return refuse(p, "omit_keys names a key this member does not default", std::format(
1065+
"mcpp.dist.apple: `options::omit_keys` names {}, which this member does not "
1066+
"write; only UIDeviceFamily, LSRequiresIPhoneOS and NSHighResolutionCapable "
1067+
"can be omitted.", key));
1068+
}
1069+
if (std::find(opt.omit_keys.begin(), opt.omit_keys.begin() + static_cast<std::ptrdiff_t>(i), key)
1070+
!= opt.omit_keys.begin() + static_cast<std::ptrdiff_t>(i)) {
1071+
return refuse(p, "omit_keys names a key twice", std::format(
1072+
"mcpp.dist.apple: `options::omit_keys` names {} twice.", key));
1073+
}
1074+
if (std::ranges::find(replacedKeys, key) != replacedKeys.end()) {
1075+
return refuse(p, "omit_keys and info_plist name one key", std::format(
1076+
"mcpp.dist.apple: `options::omit_keys` names {}, and `options::info_plist` ({}) "
1077+
"sets it; state one of the two.", key, opt.info_plist));
1078+
}
9501079
}
1080+
// An omitted default is written the way a replaced one is: not at all. The
1081+
// replaced list is what `plist_document` consults, so the two share it, and
1082+
// a defaulted key a contribution states replaces the default the same way.
1083+
for (auto const& key : opt.omit_keys) replacedKeys.push_back(key);
1084+
for (auto const& key : contributedDefaults) replacedKeys.push_back(key);
9511085
if (!opt.entitlements.empty() && !is_file(opt.entitlements)) {
9521086
return refuse(p, "entitlements not found", std::format(
9531087
"mcpp.dist.apple: the entitlements file {} was not found", opt.entitlements));

0 commit comments

Comments
 (0)