Skip to content

Commit 7026d36

Browse files
committed
0.11.0: dist-apk takes Kotlin, libraries, archives and a Maven graph; dist-apple takes Info.plist entries and an iOS device
dist-apk - options::kotlin_sources: kotlinc from xim:kotlin (feature dist-apk-kotlin) compiles the Kotlin with the Java as reference sources, before javac; the Kotlin stdlib is dexed. - R classes: aapt2 link --java, with --extra-packages for every library. - options::libraries (Android libraries from source), options::aars and options::jars, and options::maven through a lock file resolved by xim:coursier (feature dist-apk-maven). MCPP_DIST_APK_MAVEN=update and =fetch are the only packs that reach the network; an ordinary pack checks each cached artifact against the lock's sha256. - A stated subset of the manifest merger: tools:node remove/replace, tools:replace, ${applicationId}, a conflicting element refused by name. - options::sign = false writes the aligned package unsigned. dist-apple - options::info_plist: a project's entries join the Info.plist; a key the member derives is refused by name, and one it only defaults is replaced. - options::provisioning_profile, on the iOS device row: embedded as embedded.mobileprovision, its entitlements sign the bundle, and its application identifier must cover the bundle's. - The device row's runner named app is devicectl-run, from xim:apple-device-tools. mcpp::plugins::xml is the XML reader and writer the two members share. With no new option set, both members plan what 0.10.1 planned. CI bridges the three xim recipes openxlings/xim-pkgindex#844 adds until the published index carries them, and packs the Android fixtures on macOS too.
1 parent 4844e33 commit 7026d36

32 files changed

Lines changed: 2856 additions & 128 deletions

File tree

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
#!/usr/bin/env python3
2+
"""The xim recipes 0.11.0's features declare, until the published index has them.
3+
4+
TEMPORARY. `dist-apk-kotlin` declares `xim:kotlin` 2.4.20, `dist-apk-maven`
5+
declares `xim:coursier` 2.1.24, and `dist-apple` declares
6+
`xim:apple-device-tools` 0.1.0 on the iOS device row. openxlings/xim-pkgindex#844
7+
adds the three recipes. Until the index mcpp syncs carries them, this script
8+
writes the reviewed recipe files into the synced copy and adds the entries the
9+
index cache resolves `xim:<name>` through -- what `mcpp index update` would
10+
have written.
11+
12+
A recipe the synced index already carries at the version declared here is left
13+
as the index has it, and the script says so. A file this script wrote begins
14+
with a marker line, so a second run in one job does not mistake it for the
15+
index's own. When a run prints `published` for all three, the recipes are in the
16+
index, and this script and the steps that run it are deleted.
17+
18+
The files come from the pull request's head commit, named by its hash, so a CI
19+
run reads the bytes that were reviewed and not whatever a branch holds later.
20+
21+
Run after `mcpp index update`, with MCPP_HOME set.
22+
"""
23+
import json
24+
import os
25+
import re
26+
import sys
27+
import urllib.request
28+
29+
COMMIT = "8286a17724a4a4a399b23a3e109be457c052e23b"
30+
BASE = f"https://raw.githubusercontent.com/Sunrisepeak/xim-pkgindex/{COMMIT}/pkgs/"
31+
MARKER = f"-- bridged by mcpp-plugins CI from xim-pkgindex {COMMIT}\n"
32+
RECIPES = { # index path: the version this collection declares
33+
"k/kotlin.lua": "2.4.20",
34+
"c/coursier.lua": "2.1.24",
35+
"a/apple-device-tools.lua": "0.1.0",
36+
}
37+
38+
39+
def main() -> int:
40+
home = os.environ.get("MCPP_HOME")
41+
if not home:
42+
print("::error::MCPP_HOME is not set; this script edits the index under it")
43+
return 1
44+
root = os.path.join(home, "registry", "data", "xim-pkgindex")
45+
cache_path = os.path.join(root, ".xlings-index-cache.json")
46+
if not os.path.isfile(cache_path):
47+
print(f"::error::{cache_path} does not exist; run `mcpp index update` first")
48+
return 1
49+
with open(cache_path, encoding="utf-8") as f:
50+
cache = json.load(f)
51+
entries = cache["entries"]
52+
53+
changed = False
54+
for rel, version in RECIPES.items():
55+
dest = os.path.join(root, "pkgs", rel)
56+
name = os.path.basename(rel)[:-len(".lua")]
57+
key = "xim:" + name
58+
if key in entries and os.path.isfile(dest):
59+
with open(dest, encoding="utf-8") as f:
60+
present = f.read()
61+
if present.startswith(MARKER):
62+
print(f"bridged earlier in this job: {key} {version}")
63+
continue
64+
if re.search(r'\["%s"\]' % re.escape(version), present):
65+
print(f"published: the index carries {key} {version}; this bridge is not needed for it")
66+
continue
67+
with urllib.request.urlopen(BASE + rel, timeout=60) as r:
68+
text = r.read().decode("utf-8")
69+
if not re.search(r'\["%s"\]' % re.escape(version), text):
70+
print(f"::error::{BASE + rel} has no version {version}")
71+
return 1
72+
declared = re.search(r'^\s*name\s*=\s*"([^"]+)"', text, re.M).group(1)
73+
if declared != name:
74+
print(f"::error::{rel} declares the name {declared}")
75+
return 1
76+
description = re.search(r'^\s*description\s*=\s*"([^"]*)"', text, re.M).group(1)
77+
os.makedirs(os.path.dirname(dest), exist_ok=True)
78+
with open(dest, "w", encoding="utf-8", newline="\n") as f:
79+
f.write(MARKER + text)
80+
entries[key] = {
81+
"canonical_name": key, "description": description, "entry_key": key,
82+
"identity": {"name": name, "namespace": "xim"}, "name": name,
83+
"path": dest.replace("\\", "/"), "ref": "", "type": 0, "version": "",
84+
}
85+
changed = True
86+
print(f"bridged: {key} {version} from xim-pkgindex {COMMIT[:12]}")
87+
88+
if changed:
89+
with open(cache_path, "w", encoding="utf-8") as f:
90+
json.dump(cache, f, indent=1)
91+
return 0
92+
93+
94+
if __name__ == "__main__":
95+
sys.exit(main())

.github/workflows/ci.yml

Lines changed: 156 additions & 23 deletions
Large diffs are not rendered by default.

README.md

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

dist/apk.cppm

Lines changed: 1320 additions & 90 deletions
Large diffs are not rendered by default.

dist/apple.cppm

Lines changed: 301 additions & 8 deletions
Large diffs are not rendered by default.

mcpp.toml

Lines changed: 31 additions & 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.10.1"
4+
version = "0.11.0"
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"]
@@ -180,6 +180,20 @@ implies = ["surface"]
180180
sources = ["dist/apk.cppm"]
181181
implies = ["surface"]
182182

183+
# `dist-apk` WITH THE KOTLIN COMPILER, AND WITH THE MAVEN RESOLVER (0.11.0).
184+
# Features of their own rather than payloads of `dist-apk`, for the reason
185+
# `dist-appimage` gives below about downloads: provisioning runs before the
186+
# build program learns what it will compile, so a payload `dist-apk` declared
187+
# would be installed by every Android consumer, and the Kotlin compiler is 90 MB
188+
# an application with no Kotlin never runs. A project with Kotlin sources names
189+
# `dist-apk-kotlin`; one with Maven coordinates names `dist-apk-maven`; either
190+
# implies the member itself.
191+
[features.dist-apk-kotlin]
192+
implies = ["dist-apk"]
193+
194+
[features.dist-apk-maven]
195+
implies = ["dist-apk"]
196+
183197
[features.tools-embed]
184198
sources = ["tools/embed.cppm"]
185199
implies = ["surface"]
@@ -421,6 +435,13 @@ implies = ["surface"]
421435
[target.'cfg(os = "macos")'.feature-xlings.dist-apple]
422436
"xim:macapp-run" = { version = "0.1.0", when = "run" }
423437

438+
# THE iOS DEVICE ROW'S RUNNER (0.11.0), `devicectl-run`, which installs a signed
439+
# bundle on a connected device and launches it. The simulator row keeps the
440+
# runner its manifest names, so the axis excludes `env = "sim"`, and `when =
441+
# "run"` keeps a build or a pack from installing it.
442+
[target.'cfg(all(os = "ios", not(env = "sim")))'.feature-xlings.dist-apple]
443+
"xim:apple-device-tools" = { version = "0.1.0", when = "run" }
444+
424445
# ── The environment `dist-apk` needs ───────────────────────────────────────
425446
#
426447
# THE GATE IS THE TARGET ENVIRONMENT, NOT AN ACCELERATOR. `env = "android"` is
@@ -468,6 +489,15 @@ implies = ["surface"]
468489
"xim:android-debug-keystore" = "1.0.0"
469490
"xim:bundletool" = "1.18.3"
470491

492+
# The compiler `options::kotlin_sources` needs, and the resolver `options::maven`
493+
# needs, each behind its own feature (see `[features.dist-apk-kotlin]`). The
494+
# Kotlin compiler's launcher runs the JDK pinned above, the same store key.
495+
[target.'cfg(env = "android")'.feature-xlings.dist-apk-kotlin]
496+
"xim:kotlin" = "2.4.20"
497+
498+
[target.'cfg(env = "android")'.feature-xlings.dist-apk-maven]
499+
"xim:coursier" = "2.1.24"
500+
471501
[targets.plugins]
472502
kind = "lib"
473503

src/plugins.cppm

Lines changed: 166 additions & 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.10.1";
52+
inline constexpr std::string_view version = "0.11.0";
5353

5454
} // namespace mcpp::plugins
5555

@@ -98,6 +98,171 @@ inline manifest read_manifest(std::string tree) {
9898

9999
} // namespace mcpp::plugins::stage
100100

101+
// mcpp::plugins::xml -- the XML two dist members read and write.
102+
//
103+
// SHARED BECAUSE TWO DIST MEMBERS READ IT. `dist-apk` merges library manifests
104+
// into an application's, and `dist-apple` adds a project's Info.plist entries
105+
// and reads the plist a provisioning profile carries. Neither needs more than
106+
// elements, attributes and text, and neither interprets an entity, so this is
107+
// that much and no more: no namespaces resolved, no DTD read, no validation.
108+
export namespace mcpp::plugins::xml {
109+
110+
// A tree of elements and text. Comments, the XML declaration and a DOCTYPE are
111+
// read and dropped: the documents this member writes from a tree are generated
112+
// files, and a merged manifest carries no author to read a comment. Attribute
113+
// values are kept exactly as written, entities included, because nothing here
114+
// interprets them beyond comparing and substituting `${applicationId}`.
115+
struct node {
116+
std::string name; // empty: a text node
117+
std::vector<std::pair<std::string, std::string>> attrs;
118+
std::vector<node> children;
119+
std::string text; // a text node's characters
120+
};
121+
122+
struct reader {
123+
std::string_view s;
124+
std::size_t i = 0;
125+
std::string error;
126+
127+
static bool space(char c) { return c == ' ' || c == '\t' || c == '\n' || c == '\r'; }
128+
bool starts(std::string_view t) const { return s.substr(i, t.size()) == t; }
129+
void skip_space() { while (i < s.size() && space(s[i])) ++i; }
130+
bool skip_past(std::string_view end) {
131+
const auto p = s.find(end, i);
132+
if (p == std::string_view::npos) { i = s.size(); return false; }
133+
i = p + end.size();
134+
return true;
135+
}
136+
std::string name() {
137+
const std::size_t b = i;
138+
while (i < s.size() && !space(s[i]) && s[i] != '>' && s[i] != '/' && s[i] != '=') ++i;
139+
return std::string(s.substr(b, i - b));
140+
}
141+
142+
bool element(node& out) {
143+
++i; // '<'
144+
out.name = name();
145+
if (out.name.empty()) { error = "an element without a name"; return false; }
146+
for (;;) {
147+
skip_space();
148+
if (i >= s.size()) { error = "<" + out.name + "> is not terminated"; return false; }
149+
if (starts("/>")) { i += 2; return true; }
150+
if (s[i] == '>') { ++i; break; }
151+
std::string attr = name();
152+
skip_space();
153+
if (attr.empty() || i >= s.size() || s[i] != '=') {
154+
error = "an attribute of <" + out.name + "> has no value";
155+
return false;
156+
}
157+
++i;
158+
skip_space();
159+
if (i >= s.size() || (s[i] != '"' && s[i] != '\'')) {
160+
error = "attribute " + attr + " of <" + out.name + "> is not quoted";
161+
return false;
162+
}
163+
const char quote = s[i++];
164+
const auto end = s.find(quote, i);
165+
if (end == std::string_view::npos) { error = "attribute " + attr + " is not terminated"; return false; }
166+
out.attrs.emplace_back(std::move(attr), std::string(s.substr(i, end - i)));
167+
i = end + 1;
168+
}
169+
for (;;) {
170+
if (i >= s.size()) { error = "<" + out.name + "> is not closed"; return false; }
171+
if (starts("</")) {
172+
i += 2;
173+
const std::string closing = name();
174+
skip_space();
175+
if (closing != out.name || i >= s.size() || s[i] != '>') {
176+
error = "</" + closing + "> closes <" + out.name + ">";
177+
return false;
178+
}
179+
++i;
180+
return true;
181+
}
182+
if (starts("<!--")) { if (!skip_past("-->")) { error = "a comment is not terminated"; return false; } continue; }
183+
if (starts("<?")) { if (!skip_past("?>")) { error = "a processing instruction is not terminated"; return false; } continue; }
184+
if (starts("<![CDATA[")) {
185+
const std::size_t b = i;
186+
if (!skip_past("]]>")) { error = "a CDATA section is not terminated"; return false; }
187+
node t;
188+
t.text = std::string(s.substr(b, i - b));
189+
out.children.push_back(std::move(t));
190+
continue;
191+
}
192+
if (s[i] == '<') {
193+
node child;
194+
if (!element(child)) return false;
195+
out.children.push_back(std::move(child));
196+
continue;
197+
}
198+
const std::size_t b = i;
199+
auto end = s.find('<', i);
200+
if (end == std::string_view::npos) end = s.size();
201+
i = end;
202+
bool blank = true;
203+
for (std::size_t j = b; j < end; ++j) if (!space(s[j])) { blank = false; break; }
204+
if (!blank) {
205+
node t;
206+
t.text = std::string(s.substr(b, end - b));
207+
out.children.push_back(std::move(t));
208+
}
209+
}
210+
}
211+
212+
bool document(node& root) {
213+
for (;;) {
214+
skip_space();
215+
if (starts("<?")) { skip_past("?>"); continue; }
216+
if (starts("<!--")) { skip_past("-->"); continue; }
217+
if (starts("<!DOCTYPE")) { skip_past(">"); continue; }
218+
break;
219+
}
220+
if (i >= s.size() || s[i] != '<') { error = "no root element"; return false; }
221+
if (!element(root)) return false;
222+
return true;
223+
}
224+
};
225+
226+
inline bool parse(std::string_view text, node& root, std::string& error) {
227+
reader r{text};
228+
if (!r.document(root)) { error = r.error; return false; }
229+
return true;
230+
}
231+
232+
inline std::string trim_copy(const std::string& s) {
233+
std::size_t b = 0, e = s.size();
234+
while (b < e && reader::space(s[b])) ++b;
235+
while (e > b && reader::space(s[e - 1])) --e;
236+
return s.substr(b, e - b);
237+
}
238+
239+
inline void write(const node& n, std::string& out, int depth) {
240+
const std::string pad(static_cast<std::size_t>(depth) * 4, ' ');
241+
if (n.name.empty()) { out += pad + trim_copy(n.text) + "\n"; return; }
242+
out += pad + "<" + n.name;
243+
for (auto const& a : n.attrs) out += " " + a.first + "=\"" + a.second + "\"";
244+
if (n.children.empty()) { out += "/>\n"; return; }
245+
if (n.children.size() == 1 && n.children.front().name.empty()) {
246+
out += ">" + n.children.front().text + "</" + n.name + ">\n";
247+
return;
248+
}
249+
out += ">\n";
250+
for (auto const& c : n.children) write(c, out, depth + 1);
251+
out += pad + "</" + n.name + ">\n";
252+
}
253+
254+
inline std::string attr_of(const node& n, std::string_view key) {
255+
for (auto const& a : n.attrs) if (a.first == key) return a.second;
256+
return {};
257+
}
258+
259+
inline void set_attr(node& n, const std::string& key, const std::string& value) {
260+
for (auto& a : n.attrs) if (a.first == key) { a.second = value; return; }
261+
n.attrs.emplace_back(key, value);
262+
}
263+
264+
} // namespace mcpp::plugins::xml
265+
101266
// mcpp::plugins::names -- the derivations that turn a path into a C++ name.
102267
//
103268
// THESE ARE SHARED BECAUSE THEY WERE COPIED. `common_base_dir` and

tests/all-rules-compile/mcpp.toml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ plugins = { path = "../..", features = [
4444
"rules-spirv", "rules-sycl",
4545
"tools-embed", "tools-island",
4646
"dist-appimage", "dist-wix", "dist-apple", "dist-web", "dist-apk",
47+
"dist-apk-kotlin", "dist-apk-maven",
4748
], host-module = true }
4849

4950
# NO `accel`, and that is the whole design: with none, every rule returns
@@ -61,6 +62,11 @@ plugins = { path = "../..", features = [
6162
# every member's module is COMPILED on every host, and a member left out is a
6263
# member whose macOS and Windows compile is never attempted -- which is exactly
6364
# the failure the two platform-specific members are here to catch.
65+
#
66+
# `dist-apk-kotlin` and `dist-apk-maven` add no source: each implies `dist-apk`
67+
# and declares one payload on the `cfg(env = "android")` axis, which a host
68+
# build never opens. They are listed because a consumer names them, which is
69+
# what the CI step that reads this list counts.
6470

6571
[build]
6672
sources = ["src/*.cpp"]
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
maven.lock
2+
.coursier-cache/
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
// Java beside the Kotlin activity, reaching the application's R and both archives.
2+
package org.mcpp.apklibs;
3+
4+
public final class JavaHelper {
5+
private JavaHelper() {}
6+
7+
public static String describe() {
8+
return "java:" + R.string.shared_name + ":" + org.mcpp.jar.JarThing.NAME + ":" + org.mcpp.aar.AarThing.name();
9+
}
10+
}

0 commit comments

Comments
 (0)