Skip to content

Commit 35c4980

Browse files
committed
dist-apk and dist-apple collect what the resolved graph's packages contribute; one JSON reader for both (#647 E1)
1 parent e169786 commit 35c4980

18 files changed

Lines changed: 864 additions & 138 deletions

File tree

.github/workflows/ci.yml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1352,6 +1352,19 @@ jobs:
13521352
working-directory: tests/apk-consumer-libraries
13531353
run: MCPP="$MCPP" ./check-apk-libraries.sh
13541354

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

README.md

Lines changed: 58 additions & 3 deletions
Large diffs are not rendered by default.

dist/apk.cppm

Lines changed: 119 additions & 118 deletions
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,26 @@ struct options {
247247
// does, and the application's own `resources` win over every library.
248248
std::vector<library> libraries;
249249

250+
// LIBRARIES THE GRAPH CONTRIBUTES (0.12.0). With mcpp 2026.9.16.1 the root
251+
// project's build program receives the resolved graph, and every package in
252+
// it other than the application that states `[package.metadata.dist-apk]`
253+
// contributes a library of the shape above (`package`, `resources`,
254+
// `manifest`, `assets`, `java_sources`, `kotlin_sources`) and archives
255+
// (`jars`, `aars`), its paths relative to that package's directory. A
256+
// framework's library therefore reaches every application that depends on
257+
// it without being listed in the application's build program.
258+
//
259+
// RANKED BELOW THE APPLICATION'S OWN ENTRIES, and among themselves a package
260+
// above the packages it depends on: the graph lists dependencies first, so
261+
// contributions are taken in reverse. An application's `libraries` still win
262+
// a resource any contribution defines, and a library wins over the framework
263+
// beneath it, which is the order a Gradle build gives the same modules.
264+
//
265+
// `false` reads no contribution, for an application that lists every
266+
// library itself. Under an older engine, or in a dependency's own build
267+
// program, there is no graph, and nothing is contributed either way.
268+
bool graph_libraries = true;
269+
250270
// LOCAL ARCHIVES (0.11.0). A JAR joins the classpath and the dex. An AAR
251271
// contributes its classes, its resources (under the package its manifest
252272
// names), its manifest, its native libraries and its assets. Archives
@@ -1032,120 +1052,12 @@ inline bool merge_manifests(std::string& appText, const std::vector<manifest_sou
10321052
return true;
10331053
}
10341054

1035-
// ─── JSON, as far as coursier's report needs it ───────────────────────────
1036-
1037-
struct json_value {
1038-
enum class kind { null, boolean, number, string, array, object };
1039-
kind type = kind::null;
1040-
std::string text; // string, number or boolean spelling
1041-
std::vector<json_value> items;
1042-
std::vector<std::pair<std::string, json_value>> members;
1043-
1044-
const json_value* get(std::string_view key) const {
1045-
for (auto const& m : members) if (m.first == key) return &m.second;
1046-
return nullptr;
1047-
}
1048-
};
1049-
1050-
struct json_reader {
1051-
std::string_view s;
1052-
std::size_t i = 0;
1053-
1054-
void skip_space() {
1055-
while (i < s.size() && (s[i] == ' ' || s[i] == '\t' || s[i] == '\n' || s[i] == '\r')) ++i;
1056-
}
1057-
bool string(std::string& out) {
1058-
if (i >= s.size() || s[i] != '"') return false;
1059-
++i;
1060-
while (i < s.size() && s[i] != '"') {
1061-
if (s[i] != '\\') { out += s[i++]; continue; }
1062-
if (++i >= s.size()) return false;
1063-
const char e = s[i++];
1064-
switch (e) {
1065-
case 'n': out += '\n'; break;
1066-
case 't': out += '\t'; break;
1067-
case 'r': out += '\r'; break;
1068-
case 'b': out += '\b'; break;
1069-
case 'f': out += '\f'; break;
1070-
case 'u': {
1071-
if (i + 4 > s.size()) return false;
1072-
unsigned cp = 0;
1073-
for (int k = 0; k < 4; ++k) {
1074-
const char h = s[i++];
1075-
cp <<= 4;
1076-
if (h >= '0' && h <= '9') cp |= static_cast<unsigned>(h - '0');
1077-
else if (h >= 'a' && h <= 'f') cp |= static_cast<unsigned>(h - 'a' + 10);
1078-
else if (h >= 'A' && h <= 'F') cp |= static_cast<unsigned>(h - 'A' + 10);
1079-
else return false;
1080-
}
1081-
if (cp < 0x80) out += static_cast<char>(cp);
1082-
else if (cp < 0x800) { out += static_cast<char>(0xc0 | (cp >> 6)); out += static_cast<char>(0x80 | (cp & 0x3f)); }
1083-
else { out += static_cast<char>(0xe0 | (cp >> 12)); out += static_cast<char>(0x80 | ((cp >> 6) & 0x3f)); out += static_cast<char>(0x80 | (cp & 0x3f)); }
1084-
break;
1085-
}
1086-
default: out += e;
1087-
}
1088-
}
1089-
if (i >= s.size()) return false;
1090-
++i;
1091-
return true;
1092-
}
1093-
bool value(json_value& out) {
1094-
skip_space();
1095-
if (i >= s.size()) return false;
1096-
const char c = s[i];
1097-
if (c == '"') { out.type = json_value::kind::string; return string(out.text); }
1098-
if (c == '{') {
1099-
out.type = json_value::kind::object;
1100-
++i;
1101-
skip_space();
1102-
if (i < s.size() && s[i] == '}') { ++i; return true; }
1103-
for (;;) {
1104-
skip_space();
1105-
std::string key;
1106-
if (!string(key)) return false;
1107-
skip_space();
1108-
if (i >= s.size() || s[i] != ':') return false;
1109-
++i;
1110-
json_value v;
1111-
if (!value(v)) return false;
1112-
out.members.emplace_back(std::move(key), std::move(v));
1113-
skip_space();
1114-
if (i < s.size() && s[i] == ',') { ++i; continue; }
1115-
if (i < s.size() && s[i] == '}') { ++i; return true; }
1116-
return false;
1117-
}
1118-
}
1119-
if (c == '[') {
1120-
out.type = json_value::kind::array;
1121-
++i;
1122-
skip_space();
1123-
if (i < s.size() && s[i] == ']') { ++i; return true; }
1124-
for (;;) {
1125-
json_value v;
1126-
if (!value(v)) return false;
1127-
out.items.push_back(std::move(v));
1128-
skip_space();
1129-
if (i < s.size() && s[i] == ',') { ++i; continue; }
1130-
if (i < s.size() && s[i] == ']') { ++i; return true; }
1131-
return false;
1132-
}
1133-
}
1134-
const std::size_t b = i;
1135-
while (i < s.size() && s[i] != ',' && s[i] != '}' && s[i] != ']' &&
1136-
s[i] != ' ' && s[i] != '\n' && s[i] != '\r' && s[i] != '\t') ++i;
1137-
out.text = std::string(s.substr(b, i - b));
1138-
if (out.text == "null") out.type = json_value::kind::null;
1139-
else if (out.text == "true" || out.text == "false") out.type = json_value::kind::boolean;
1140-
else out.type = json_value::kind::number;
1141-
return !out.text.empty();
1142-
}
1143-
};
1144-
1145-
inline bool parse_json(std::string_view text, json_value& out) {
1146-
json_reader r{text};
1147-
return r.value(out);
1148-
}
1055+
// ─── JSON ─────────────────────────────────────────────────────────────────
1056+
//
1057+
// coursier's report is read with the collection's shared reader, the one
1058+
// `mcpp::plugins::graph` reads the engine's graph document with.
1059+
using json_value = mcpp::plugins::json::value;
1060+
using mcpp::plugins::json::parse_json;
11491061

11501062
// ─── The Maven lock ───────────────────────────────────────────────────────
11511063

@@ -1488,6 +1400,88 @@ inline plan plan_for(options opt = {}) {
14881400
}
14891401
}
14901402

1403+
// ── the libraries the graph contributes (0.12.0) ─────────────────────
1404+
//
1405+
// Appended after the application's own `libraries`, `jars` and `aars`,
1406+
// which keeps them highest; the graph's packages are taken requesters first
1407+
// (the reverse of the document's order). Their paths are made absolute
1408+
// against each package's directory here, so the manifest-relative
1409+
// resolution below leaves them as they are. `labels` names each library in
1410+
// a diagnostic: empty for the application's own.
1411+
std::vector<std::string> labels(opt.libraries.size());
1412+
if (opt.graph_libraries) {
1413+
auto graph = mcpp::plugins::graph::read();
1414+
if (!graph) {
1415+
return refuse(p, "unreadable graph document", std::format(
1416+
"mcpp.dist.apk: {}; the libraries its packages contribute cannot be collected. "
1417+
"Set options::graph_libraries = false to list every library in options::libraries.",
1418+
graph.error()));
1419+
}
1420+
for (std::size_t k = graph->packages.size(); k-- > 0;) {
1421+
const auto& pkg = graph->packages[k];
1422+
if (pkg.root) continue;
1423+
const json_value* t = mcpp::plugins::graph::table_of(pkg, "dist-apk");
1424+
if (!t) continue;
1425+
const std::string who = std::format("the [package.metadata.dist-apk] of {}",
1426+
mcpp::plugins::graph::label_of(pkg));
1427+
library l;
1428+
std::vector<std::string> jarList, aarList;
1429+
for (auto const& [key, v] : t->members) {
1430+
const bool isString = v.type == json_value::kind::string;
1431+
const bool isList = v.type == json_value::kind::array
1432+
&& std::ranges::all_of(v.items, [](const json_value& e) {
1433+
return e.type == json_value::kind::string; });
1434+
const auto wants = [&](bool ok, const char* shape) -> bool {
1435+
if (!ok) {
1436+
refuse(p, "malformed graph contribution", std::format(
1437+
"mcpp.dist.apk: {} states `{}` as something other than {}.",
1438+
who, key, shape));
1439+
}
1440+
return ok;
1441+
};
1442+
const auto strings = [&]() {
1443+
std::vector<std::string> out;
1444+
for (auto const& e : v.items) out.push_back(mcpp::plugins::graph::resolve(pkg, e.text));
1445+
return out;
1446+
};
1447+
if (key == "package") {
1448+
if (!wants(isString, "a string")) return p;
1449+
l.package = v.text;
1450+
} else if (key == "resources" || key == "manifest" || key == "assets") {
1451+
if (!wants(isString, "a path")) return p;
1452+
const std::string path = mcpp::plugins::graph::resolve(pkg, v.text);
1453+
(key == "resources" ? l.resources : key == "manifest" ? l.manifest : l.assets) = path;
1454+
} else if (key == "java_sources" || key == "kotlin_sources"
1455+
|| key == "jars" || key == "aars") {
1456+
if (!wants(isList, "a list of paths")) return p;
1457+
auto list = strings();
1458+
if (key == "java_sources") l.java_sources = std::move(list);
1459+
else if (key == "kotlin_sources") l.kotlin_sources = std::move(list);
1460+
else if (key == "jars") jarList = std::move(list);
1461+
else aarList = std::move(list);
1462+
} else {
1463+
// Warned, not refused: a package published for a newer
1464+
// collection may state a key this one does not read, and
1465+
// refusing it would break every application built with this
1466+
// version.
1467+
mcpp::warning(std::format(
1468+
"mcpp.dist.apk: {} states `{}`, which this version of the member does "
1469+
"not read; it is ignored.", who, key).c_str());
1470+
}
1471+
}
1472+
const bool isLibrary = !l.package.empty() || !l.resources.empty() || !l.manifest.empty()
1473+
|| !l.assets.empty() || !l.java_sources.empty() || !l.kotlin_sources.empty();
1474+
if (isLibrary) {
1475+
opt.libraries.push_back(std::move(l));
1476+
labels.push_back(std::format("the library {} ({})",
1477+
opt.libraries.back().package.empty() ? mcpp::plugins::graph::label_of(pkg)
1478+
: opt.libraries.back().package, who));
1479+
}
1480+
for (auto& j : jarList) opt.jars.push_back(std::move(j));
1481+
for (auto& a : aarList) opt.aars.push_back(std::move(a));
1482+
}
1483+
}
1484+
14911485
// Every path option, resolved against the manifest once (`resolve_path`).
14921486
opt.resources = resolve_path(opt.resources);
14931487
resolve_paths(opt.java_sources);
@@ -1632,7 +1626,9 @@ inline plan plan_for(options opt = {}) {
16321626
for (std::size_t i = 0; i < opt.libraries.size(); ++i) {
16331627
const auto& l = opt.libraries[i];
16341628
library_input in;
1635-
in.label = l.package.empty() ? std::format("options::libraries[{}]", i)
1629+
in.label = i < labels.size() && !labels[i].empty()
1630+
? labels[i]
1631+
: l.package.empty() ? std::format("options::libraries[{}]", i)
16361632
: std::format("the library {}", l.package);
16371633
in.package = l.package;
16381634
if (!l.resources.empty()) {
@@ -1642,9 +1638,14 @@ inline plan plan_for(options opt = {}) {
16421638
in.label, l.resources));
16431639
}
16441640
if (l.package.empty()) {
1645-
return refuse(p, "library resources without a package", std::format(
1646-
"mcpp.dist.apk: options::libraries[{}] has resources and no package; its "
1647-
"R class needs one. Set options::libraries[{}].package.", i, i));
1641+
return refuse(p, "library resources without a package",
1642+
i < labels.size() && !labels[i].empty()
1643+
? std::format(
1644+
"mcpp.dist.apk: {} has resources and no package; its R class needs "
1645+
"one. Set `package` in that [package.metadata.dist-apk] table.", labels[i])
1646+
: std::format(
1647+
"mcpp.dist.apk: options::libraries[{}] has resources and no package; its "
1648+
"R class needs one. Set options::libraries[{}].package.", i, i));
16481649
}
16491650
in.resources = l.resources;
16501651
}

0 commit comments

Comments
 (0)