Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 32 additions & 6 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ env:
# through the runner named after its format, which is how `mcpp run --format
# app` reaches `macapp-run`. An engine below it stages no `needs` lines, and
# `dist-apk` refuses its tree naming this release.
MCPP_VERSION: 2026.9.14.2
MCPP_VERSION: 2026.9.16.1
# AN ENGINE BUILT FROM SOURCE, WHEN A DISPATCH NAMES ONE.
#
# Empty on every push and pull request, so the steps run the release above.
Expand Down Expand Up @@ -1249,11 +1249,12 @@ jobs:
# does; `dist-apk` provisions the four payloads on its own
# `feature-xlings.dist-apk` table (`xim:android-build-tools`, `xim:
# android-platform`, `xim:jdk-temurin`, `xim:android-debug-keystore`).
# NOT YET GREEN under this job's pinned `MCPP_VERSION`: `kind = "app"`
# and `mcpp::min_platform_version()` are #622 engine additions, and
# this step needs the release that carries them (see this repository's
# README, `dist-apk`'s own row) -- raising the pin is P5's own change,
# together with the rest of this collection's release.
# `kind = "app"` and `mcpp::min_platform_version()` are #622 engine
# additions, and this step needed the release that carries them; the pin
# has been above it since 2026.9.14.2 and the step is green. From
# 2026.9.16.1 the pin also carries the engine's strip decision
# (`MCPP_PACK_STRIP`, `MCPP_PACK_DEBUG_SYMBOLS_DIR`), which is what lets
# legs (m) and (n) below run rather than report themselves skipped.
#
# THIS STEP ASSERTS PACKAGING ONLY, DELIBERATELY, NOT `mcpp run
# --format apk`. This fixture's own `main.cpp` and `mcpp.toml` DO wire
Expand Down Expand Up @@ -1352,6 +1353,19 @@ jobs:
working-directory: tests/apk-consumer-libraries
run: MCPP="$MCPP" ./check-apk-libraries.sh

# LIBRARIES THE GRAPH CONTRIBUTES (0.12.0). `tests/apk-consumer-graph/lib`
# states `[package.metadata.dist-apk]`, and the application names it only
# as a dependency. Under an engine that gives the root build program the
# resolved graph (mcpp 2026.9.16.1+) the library's resource is in the APK,
# `graph_libraries = false` reads none, the application's own library wins
# a resource both define, and a malformed contribution is refused naming
# the package and the key; under an older engine the pack succeeds without
# the contribution. The script chooses its legs from `mcpp --version`.
- name: dist-apk collects the libraries the resolved graph contributes
timeout-minutes: 30
working-directory: tests/apk-consumer-graph
run: MCPP="$MCPP" ./check-apk-graph.sh

# Compiles the device unit on a machine with no GPU: the clang route
# produces sm_89 code from the payload toolkit. Running it needs a
# device, so the run is of the CPU variant, which the same seam serves.
Expand Down Expand Up @@ -2046,6 +2060,18 @@ jobs:
working-directory: tests/metal-consumer
run: MCPP="$MCPP" ./check-metal.sh

# rules-swift (mcpp#647 E2): a C++ program calling a Swift function, which
# calls back into C through the bridging header, printing a line from each
# side; the generated header declares the exported function; the fixture
# links for the iOS simulator when the runner has that SDK. On a runner
# without swiftc the script asserts the rule's refusal and annotates the
# run instead.
- name: rules-swift compiles a package's Swift sources, or records that this runner has no swiftc
if: ${{ !cancelled() && runner.os == 'macOS' }}
timeout-minutes: 20
working-directory: tests/swift-consumer
run: MCPP="$MCPP" ./check-swift.sh

# THE iOS ROW, FOR REAL (#622 B1's other half). `tests/ios-app-consumer`
# is the same fixture the Linux `consumers` job checks at the plan
# level (see that fixture's own header); here it is built, packed and
Expand Down
67 changes: 63 additions & 4 deletions README.md

Large diffs are not rendered by default.

365 changes: 236 additions & 129 deletions dist/apk.cppm

Large diffs are not rendered by default.

164 changes: 149 additions & 15 deletions dist/apple.cppm
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,35 @@ struct options {
// `NSHighResolutionCapable`) are replaced by the project's value.
std::string info_plist;

// DEFAULTED Info.plist KEYS TO LEAVE OUT (0.12.0). A property list has no
// null, so `info_plist` can replace a default's value but cannot remove the
// key; a project whose other build states neither `NSHighResolutionCapable`
// nor `LSRequiresIPhoneOS` names them here (#649 P1). Only the three keys
// this member defaults are accepted: a key it derives is refused by name, as
// `info_plist` refuses it, and so is any other key, which this member never
// writes. A key named here and also set by `info_plist` is refused, because
// the two statements contradict each other. A key that does not apply to the
// row (`UIDeviceFamily` on macOS) is accepted and changes nothing.
std::vector<std::string> omit_keys;

// Info.plist ENTRIES THE GRAPH CONTRIBUTES (0.12.0). With mcpp 2026.9.16.1
// the root project's build program receives the resolved graph, and every
// package in it other than the application that states
// `[package.metadata.dist-apple] info_plist = "<file>"` contributes that
// plist's entries, the path relative to the package's directory: a library
// that needs a usage description or a background mode states it once, and
// every application that depends on it carries it. The entries are applied
// in the graph's order, dependencies first, so a package overrides the
// packages it depends on, and the application's own `info_plist` is applied
// last and wins every key. The keys this member derives are refused in a
// contribution as in `info_plist`, naming the package; a key the application
// names in `omit_keys` is left out whoever contributes it.
//
// `false` reads no contribution. Under an older engine, or in a dependency's
// own build program, there is no graph, and nothing is contributed either
// way.
bool graph_info_plist = true;

// AN iOS DEVICE BUNDLE'S PROVISIONING PROFILE (0.11.0), manifest-relative or
// absolute: embedded as `embedded.mobileprovision`, and, when `entitlements`
// is empty, the source of the entitlements the bundle is signed with (the
Expand Down Expand Up @@ -662,44 +691,52 @@ inline bool dict_entries(const mcpp::plugins::xml::node& dict,
return true;
}

// Reads `options::info_plist`: the entries to add, as the plist text
// `plist_document` inserts, and the defaulted keys they replace.
inline bool read_info_plist_fragment(const std::string& path, std::string& entries,
std::vector<std::string>& replaced, std::string& message) {
// One Info.plist entry a fragment states: its key and the plist text of the
// `<key>` and its value, as `plist_document` inserts it.
struct plist_entry {
std::string key;
std::string text;
};

// Reads one plist fragment's entries. `who` is how a diagnostic names the
// fragment (`options::info_plist`, or a package's [package.metadata.dist-apple]).
inline bool read_plist_entries(const std::string& path, const std::string& who,
std::vector<plist_entry>& out, std::string& message) {
namespace xml = mcpp::plugins::xml;
xml::node root;
std::string err;
if (!xml::parse(read_text(path), root, err)) {
message = std::format("mcpp.dist.apple: `options::info_plist` ({}) cannot be read: {}", path, err);
message = std::format("mcpp.dist.apple: {} ({}) cannot be read: {}", who, path, err);
return false;
}
const xml::node* dict = top_dict(root);
if (!dict) {
message = std::format("mcpp.dist.apple: `options::info_plist` ({}) has no top-level <dict>.", path);
message = std::format("mcpp.dist.apple: {} ({}) has no top-level <dict>.", who, path);
return false;
}
std::vector<std::pair<std::string, const xml::node*>> pairs;
if (!dict_entries(*dict, pairs, err)) {
message = std::format("mcpp.dist.apple: `options::info_plist` ({}): {}", path, err);
message = std::format("mcpp.dist.apple: {} ({}): {}", who, path, err);
return false;
}
std::vector<std::string> seen;
for (auto const& [key, value] : pairs) {
if (std::ranges::find(derived_plist_keys(), key) != derived_plist_keys().end()) {
message = std::format(
"mcpp.dist.apple: `options::info_plist` ({}) sets {}, which this member derives "
"from its options and the engine; set the option instead.", path, key);
"mcpp.dist.apple: {} ({}) sets {}, which this member derives "
"from its options and the engine; set the option instead.", who, path, key);
return false;
}
if (std::ranges::find(seen, key) != seen.end()) {
message = std::format("mcpp.dist.apple: `options::info_plist` ({}) sets {} twice.", path, key);
message = std::format("mcpp.dist.apple: {} ({}) sets {} twice.", who, path, key);
return false;
}
seen.push_back(key);
if (std::ranges::find(defaulted_plist_keys(), key) != defaulted_plist_keys().end())
replaced.push_back(key);
entries += " <key>" + key + "</key>\n";
xml::write(*value, entries, 1);
plist_entry e;
e.key = key;
e.text = " <key>" + key + "</key>\n";
xml::write(*value, e.text, 1);
out.push_back(std::move(e));
}
return true;
}
Expand Down Expand Up @@ -938,16 +975,113 @@ inline plan plan_for(options opt = {}) {
opt.provisioning_profile = resolve_path(opt.provisioning_profile);
std::string extraEntries;
std::vector<std::string> replacedKeys;
std::vector<std::string> contributedDefaults;
// The application's own entries, read first so that their refusals come
// before any contribution's; they are applied last.
std::vector<plist_entry> ownEntries;
if (!opt.info_plist.empty()) {
if (!is_file(opt.info_plist)) {
return refuse(p, "info_plist not found", std::format(
"mcpp.dist.apple: `options::info_plist` ({}) was not found", opt.info_plist));
}
mcpp::rerun_if_changed(opt.info_plist.c_str());
std::string message;
if (!read_info_plist_fragment(opt.info_plist, extraEntries, replacedKeys, message))
if (!read_plist_entries(opt.info_plist, "`options::info_plist`", ownEntries, message))
return refuse(p, "unusable info_plist", message);
for (auto const& e : ownEntries)
if (std::ranges::find(defaulted_plist_keys(), e.key) != defaulted_plist_keys().end())
replacedKeys.push_back(e.key);
}

// The entries the graph's packages contribute, in the graph's order
// (dependencies first), each replacing an earlier contribution of the same
// key; then the application's own, replacing any contribution.
std::vector<plist_entry> merged;
const auto apply_entries = [&](const std::vector<plist_entry>& list) {
for (auto const& e : list) {
auto it = std::ranges::find_if(merged, [&](const plist_entry& m) { return m.key == e.key; });
if (it != merged.end()) it->text = e.text;
else merged.push_back(e);
}
};
if (opt.graph_info_plist) {
auto graph = mcpp::plugins::graph::read();
if (!graph) {
return refuse(p, "unreadable graph document", std::format(
"mcpp.dist.apple: {}; the Info.plist entries its packages contribute cannot be "
"collected. Set `options::graph_info_plist = false` to read none.", graph.error()));
}
for (auto const& pkg : graph->packages) {
if (pkg.root) continue;
const auto* t = mcpp::plugins::graph::table_of(pkg, "dist-apple");
if (!t) continue;
const std::string who = std::format("the [package.metadata.dist-apple] of {}",
mcpp::plugins::graph::label_of(pkg));
for (auto const& [key, v] : t->members) {
if (key == "info_plist") continue;
mcpp::warning(std::format(
"mcpp.dist.apple: {} states `{}`, which this version of the member does not "
"read; it is ignored.", who, key).c_str());
}
const auto* ip = t->get("info_plist");
if (!ip) continue;
if (ip->type != mcpp::plugins::json::value::kind::string || ip->text.empty()) {
return refuse(p, "malformed graph contribution", std::format(
"mcpp.dist.apple: {} states `info_plist` as something other than a path.", who));
}
const std::string path = mcpp::plugins::graph::resolve(pkg, ip->text);
if (!is_file(path)) {
return refuse(p, "contributed info_plist not found", std::format(
"mcpp.dist.apple: {} names the Info.plist fragment {}, which was not found.", who, path));
}
mcpp::rerun_if_changed(path.c_str());
std::vector<plist_entry> list;
std::string message;
if (!read_plist_entries(path, who, list, message))
return refuse(p, "unusable contributed info_plist", message);
apply_entries(list);
}
}
// The application's own entries win; a key it omits is left out whoever
// contributed it. The omit_keys checks below still see only the
// application's own entries in `replacedKeys`.
apply_entries(ownEntries);
for (auto const& e : merged) {
if (std::ranges::find(opt.omit_keys, e.key) != opt.omit_keys.end()) continue;
extraEntries += e.text;
if (std::ranges::find(defaulted_plist_keys(), e.key) != defaulted_plist_keys().end()
&& std::ranges::find(replacedKeys, e.key) == replacedKeys.end())
contributedDefaults.push_back(e.key);
}
for (std::size_t i = 0; i < opt.omit_keys.size(); ++i) {
const auto& key = opt.omit_keys[i];
if (std::ranges::find(derived_plist_keys(), key) != derived_plist_keys().end()) {
return refuse(p, "omit_keys names a derived key", std::format(
"mcpp.dist.apple: `options::omit_keys` names {}, which this member derives "
"from its options and the engine; a derived key cannot be omitted.", key));
}
if (std::ranges::find(defaulted_plist_keys(), key) == defaulted_plist_keys().end()) {
return refuse(p, "omit_keys names a key this member does not default", std::format(
"mcpp.dist.apple: `options::omit_keys` names {}, which this member does not "
"write; only UIDeviceFamily, LSRequiresIPhoneOS and NSHighResolutionCapable "
"can be omitted.", key));
}
if (std::find(opt.omit_keys.begin(), opt.omit_keys.begin() + static_cast<std::ptrdiff_t>(i), key)
!= opt.omit_keys.begin() + static_cast<std::ptrdiff_t>(i)) {
return refuse(p, "omit_keys names a key twice", std::format(
"mcpp.dist.apple: `options::omit_keys` names {} twice.", key));
}
if (std::ranges::find(replacedKeys, key) != replacedKeys.end()) {
return refuse(p, "omit_keys and info_plist name one key", std::format(
"mcpp.dist.apple: `options::omit_keys` names {}, and `options::info_plist` ({}) "
"sets it; state one of the two.", key, opt.info_plist));
}
}
// An omitted default is written the way a replaced one is: not at all. The
// replaced list is what `plist_document` consults, so the two share it, and
// a defaulted key a contribution states replaces the default the same way.
for (auto const& key : opt.omit_keys) replacedKeys.push_back(key);
for (auto const& key : contributedDefaults) replacedKeys.push_back(key);
if (!opt.entitlements.empty() && !is_file(opt.entitlements)) {
return refuse(p, "entitlements not found", std::format(
"mcpp.dist.apple: the entitlements file {} was not found", opt.entitlements));
Expand Down
Loading
Loading