Skip to content

Commit a11c63e

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. The Android fixtures build on Linux: mcpp 2026.9.14.2 does not link an Android row on a macOS host (mcpp-community/mcpp#647, E3).
1 parent 4844e33 commit a11c63e

32 files changed

Lines changed: 2839 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: 134 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1219,6 +1219,20 @@ jobs:
12191219
- name: dist-web's second pack copies nothing
12201220
working-directory: tests/web-consumer
12211221
run: MCPP="$MCPP" ./check-web-idempotent.sh
1222+
1223+
# TEMPORARY, UNTIL openxlings/xim-pkgindex#844 IS IN THE PUBLISHED INDEX.
1224+
# 0.11.0's `dist-apk-kotlin` and `dist-apk-maven` declare `xim:kotlin` and
1225+
# `xim:coursier`, which only that pull request adds (with
1226+
# `xim:apple-device-tools`). The script writes the reviewed recipes into
1227+
# the synced index and does nothing for a recipe the index already
1228+
# carries; see its header for when it and this step are deleted. Here,
1229+
# right before the Android steps, because the index is synced again when
1230+
# a dependency misses and a sync rewrites the cache this edits.
1231+
- name: bridge the xim recipes the published index does not carry yet
1232+
run: |
1233+
"$MCPP" index update
1234+
python3 .github/scripts/bridge-xim-recipes.py
1235+
12221236
# `kind = "app"` on `*-linux-android` links a shared object (#622 A3),
12231237
# so this fixture's own toolchain resolution provisions `xim:android-
12241238
# ndk` for `--target x86_64-linux-android` exactly as mcpp's own CI
@@ -1288,8 +1302,10 @@ jobs:
12881302
# reached by an absolute path, dex into one file carrying classes from
12891303
# both; (g) a project's own res/ links as the BASE -- two resources
12901304
# nothing else defines reach the apk (0.9.0 linked res/ as an aapt2
1291-
# overlay, which refuses any resource the base does not already have).
1292-
- name: dist-apk's manifest template, Java roots, res/ and version tokens, (a) to (h)
1305+
# overlay, which refuses any resource the base does not already have);
1306+
# (i) Kotlin sources without the `dist-apk-kotlin` feature are refused
1307+
# naming it, and (j) `sign = false` with a keystore is refused (0.11.0).
1308+
- name: dist-apk's manifest template, Java roots, res/, version tokens and refusals, (a) to (j)
12931309
working-directory: tests/apk-consumer
12941310
run: MCPP="$MCPP" ./check-apk-features.sh
12951311

@@ -1308,6 +1324,24 @@ jobs:
13081324
working-directory: tests/apk-consumer-shared
13091325
run: MCPP="$MCPP" ./check-apk-closure.sh
13101326

1327+
# 0.11.0: KOTLIN, R CLASSES, LIBRARIES, ARCHIVES, A MAVEN GRAPH AND AN
1328+
# UNSIGNED PACKAGE. One fixture reaches every configuration through the
1329+
# environment variables its script sets: an application of Kotlin and
1330+
# Java, a library from source, a local AAR and JAR in one signed APK
1331+
# (every class in the dex, the three R packages, the application's value
1332+
# winning a resource both define, the merged manifest, the archive's
1333+
# native library and assets); `sign = false`; an App Bundle from the same
1334+
# inputs; a manifest conflict refused by name; and a Maven graph through
1335+
# its lock -- refused without one, resolved by
1336+
# `MCPP_DIST_APK_MAVEN=update`, read from the cache by an ordinary pack,
1337+
# refused with an empty cache, restored by `MCPP_DIST_APK_MAVEN=fetch`,
1338+
# and refused when the lock is stale. The two explicit modes are the only
1339+
# packs that reach the network.
1340+
- name: dist-apk compiles Kotlin beside Java and packs libraries, archives and a Maven graph
1341+
timeout-minutes: 30
1342+
working-directory: tests/apk-consumer-libraries
1343+
run: MCPP="$MCPP" ./check-apk-libraries.sh
1344+
13111345
# Compiles the device unit on a machine with no GPU: the clang route
13121346
# produces sm_89 code from the payload toolkit. Running it needs a
13131347
# device, so the run is of the CPU variant, which the same seam serves.
@@ -1369,25 +1403,26 @@ jobs:
13691403
# `<name> = { ... }`. Reading keys out of the table BODY matched
13701404
# `sources` once per feature and reported the fixture as
13711405
# incomplete while the extractor was what broke.
1372-
# MEMBERS, NOT EVERY FEATURE. A feature that another feature
1373-
# IMPLIES is internal -- `surface` carries the build-program half of
1374-
# the generated surface and every member implies it, and a consumer
1375-
# never writes it. Subtracting the implied ones keeps this check
1406+
# MEMBERS, NOT EVERY FEATURE. A member is a feature named for its
1407+
# family -- `rules-*`, `tools-*`, `dist-*`, the README's naming table
1408+
# -- and a consumer names it. `surface` is not one: it carries the
1409+
# build-program half of the generated surface, every member implies
1410+
# it, and a consumer never writes it. Keeping it out keeps this check
13761411
# meaning "the fixture names every member a consumer can activate";
13771412
# padding the fixture instead would make it mean less.
13781413
#
1379-
# The rule is read out of the manifest, not listed here, so a seventh
1380-
# MEMBER is still caught: nothing implies it.
1414+
# BY NAME, NOT BY "NOTHING IMPLIES IT". That was the rule until 0.11.0,
1415+
# when `dist-apk-kotlin` and `dist-apk-maven` came to imply `dist-apk`,
1416+
# which a consumer still names. The rule is read out of the manifest,
1417+
# not listed here, so a new member is still caught.
13811418
# A TEMP FILE, NOT `<(...)`. This step also runs on windows-2022
13821419
# through Git Bash, where process substitution is emulated and not
13831420
# dependable -- and a check that behaves differently on one of the
13841421
# three hosts is the exact class of difference this job exists to
13851422
# catch, so it must not introduce one.
1386-
grep -oE '^implies[[:space:]]*=.*' mcpp.toml \
1387-
| grep -oE '"[a-z0-9-]+"' | tr -d '"' | sort -u > /tmp/implied.txt
13881423
grep -oE '^\[features\.[a-z0-9-]+\]' mcpp.toml \
1389-
| sed 's/^\[features\.//; s/\]$//' | sort > /tmp/allfeats.txt
1390-
feats=$(comm -23 /tmp/allfeats.txt /tmp/implied.txt)
1424+
| sed 's/^\[features\.//; s/\]$//' | grep -E '^(rules|tools|dist)-' | sort > /tmp/members.txt
1425+
feats=$(cat /tmp/members.txt)
13911426
used=$(sed -n '/features = \[/,/\], host-module/p' tests/all-rules-compile/mcpp.toml \
13921427
| grep -oE '"[a-z-]+"' | tr -d '"' | sort)
13931428
[ -n "$feats" ] || {
@@ -1710,23 +1745,20 @@ jobs:
17101745
# `<name> = { ... }`. Reading keys out of the table BODY matched
17111746
# `sources` once per feature and reported the fixture as
17121747
# incomplete while the extractor was what broke.
1713-
# MEMBERS, NOT EVERY FEATURE. A feature that another feature
1714-
# IMPLIES is internal -- `surface` carries the build-program half of
1715-
# the generated surface and every member implies it, and a consumer
1716-
# never writes it. Subtracting the implied ones keeps this check
1717-
# meaning "the fixture names every member a consumer can activate";
1718-
# padding the fixture instead would make it mean less. The rule is
1719-
# read out of the manifest, so a seventh MEMBER is still caught.
1748+
# MEMBERS, NOT EVERY FEATURE: a feature named for its family
1749+
# (`rules-*`, `tools-*`, `dist-*`), which a consumer names; `surface`,
1750+
# which every member implies and no consumer writes, is not one. By
1751+
# name and not by "nothing implies it" since 0.11.0, when
1752+
# `dist-apk-kotlin` and `dist-apk-maven` came to imply `dist-apk`. The
1753+
# rule is read out of the manifest, so a new member is still caught.
17201754
#
17211755
# A temp file, not `<(...)`: this step also runs on windows-2022
17221756
# through Git Bash, where process substitution is emulated and not
17231757
# dependable -- and a check that behaves differently on one of the
17241758
# three hosts is the class of difference this job exists to catch.
1725-
grep -oE '^implies[[:space:]]*=.*' mcpp.toml \
1726-
| grep -oE '"[a-z0-9-]+"' | tr -d '"' | sort -u > /tmp/implied.txt
17271759
grep -oE '^\[features\.[a-z0-9-]+\]' mcpp.toml \
1728-
| sed 's/^\[features\.//; s/\]$//' | sort > /tmp/allfeats.txt
1729-
feats=$(comm -23 /tmp/allfeats.txt /tmp/implied.txt)
1760+
| sed 's/^\[features\.//; s/\]$//' | grep -E '^(rules|tools|dist)-' | sort > /tmp/members.txt
1761+
feats=$(cat /tmp/members.txt)
17301762
used=$(sed -n '/features = \[/,/\], host-module/p' tests/all-rules-compile/mcpp.toml \
17311763
| grep -oE '"[a-z-]+"' | tr -d '"' | sort)
17321764
[ -n "$feats" ] || {
@@ -1950,6 +1982,37 @@ jobs:
19501982
grep -q 'closure = not-walked' pack.log || echo "note: the stage manifest's closure line was not echoed by pack"
19511983
echo "ok: one bundle, a valid plist, it launches, and the resource is where NSBundle looks"
19521984
1985+
# 0.11.0 (`options::info_plist`): a project's Info.plist entries join the
1986+
# generated plist of the real bundle, a key the member only defaults
1987+
# (`NSHighResolutionCapable`) takes the project's value, Apple's parser
1988+
# accepts the result, and the bundle still launches. The refusal of a key
1989+
# the member derives is asserted at the plan level, in the Linux job.
1990+
- name: dist-apple carries a project's Info.plist entries, and the bundle launches
1991+
if: ${{ !cancelled() && runner.os == 'macOS' }}
1992+
working-directory: tests/app-consumer
1993+
run: |
1994+
set -e
1995+
export APP_CONSUMER_INFO_PLIST="$PWD/info-plist/usage.plist"
1996+
"$MCPP" pack --format app | tee pack-info.log
1997+
app=$(find target -name '*.app' -type d | head -1)
1998+
test -n "$app" || { cat pack-info.log; echo "FAIL: no .app bundle"; exit 1; }
1999+
plist="$app/Contents/Info.plist"
2000+
plutil -lint "$plist"
2001+
pb() { /usr/libexec/PlistBuddy -c "Print :$1" "$plist"; }
2002+
[ "$(pb NSCameraUsageDescription)" = "Scans the codes a user points the camera at." ] \
2003+
|| { echo "FAIL: the usage description did not reach the bundle"; cat "$plist"; exit 1; }
2004+
[ "$(pb CFBundleURLTypes:0:CFBundleURLSchemes:0)" = "mcpp-fixture" ] \
2005+
|| { echo "FAIL: the URL type did not reach the bundle"; cat "$plist"; exit 1; }
2006+
[ "$(pb NSHighResolutionCapable)" = "false" ] \
2007+
|| { echo "FAIL: the project's NSHighResolutionCapable did not replace the default"; cat "$plist"; exit 1; }
2008+
[ "$(grep -c '<key>NSHighResolutionCapable</key>' "$plist")" -eq 1 ] \
2009+
|| { echo "FAIL: NSHighResolutionCapable is stated twice"; cat "$plist"; exit 1; }
2010+
[ "$(pb CFBundleExecutable)" = "app-consumer" ] \
2011+
|| { echo "FAIL: CFBundleExecutable is no longer the member's"; cat "$plist"; exit 1; }
2012+
"$app/Contents/MacOS/app-consumer" | grep -q '^app-consumer ok' \
2013+
|| { echo "FAIL: the bundle does not launch with the project's entries"; exit 1; }
2014+
echo "ok: the project's entries are in the bundle's Info.plist, plutil accepts it, and the bundle launches"
2015+
19532016
# THE CLOSURE AS A FRAMEWORK, ON THE BUNDLE (mcpp#634, B1 to B3). The
19542017
# plan-level half runs on Linux. This step, the Metal step and the iOS
19552018
# step measure independent things, so each runs when an earlier step of
@@ -2070,6 +2133,54 @@ jobs:
20702133
|| { echo "FAIL: the program's output line '1-2-3' is absent"; printf '%s\n' "$out" | tail -8 | od -c; exit 1; }
20712134
echo "ok: a flat iOS Simulator bundle, MinimumOSVersion 17.0, and the simulator ran it and returned its status 7"
20722135
2136+
# 0.11.0 (`options::info_plist`) on the iOS row: the project's entries
2137+
# join the simulator bundle's Info.plist, its `UIDeviceFamily` replaces
2138+
# the member's default, and the simulator installs and runs the bundle.
2139+
- name: dist-apple's iOS row carries a project's Info.plist entries, and the simulator runs the bundle
2140+
if: ${{ !cancelled() && runner.os == 'macOS' }}
2141+
working-directory: tests/ios-app-consumer
2142+
run: |
2143+
set -uo pipefail
2144+
if ! xcrun --sdk iphonesimulator --show-sdk-path >/dev/null 2>&1; then
2145+
echo "SKIP: no iphonesimulator SDK on this runner"; exit 0
2146+
fi
2147+
set -e
2148+
export IOS_APP_CONSUMER_INFO_PLIST="$PWD/info-plist/usage.plist"
2149+
"$MCPP" pack --target aarch64-ios-sim --format app | tee pack-info.log
2150+
app=$(find target -name '*.app' -type d | head -1)
2151+
test -n "$app" || { cat pack-info.log; echo "FAIL: no .app bundle"; exit 1; }
2152+
plist="$app/Info.plist"
2153+
plutil -lint "$plist"
2154+
pb() { /usr/libexec/PlistBuddy -c "Print :$1" "$plist"; }
2155+
[ "$(pb NSCameraUsageDescription)" = "Scans the codes a user points the camera at." ] \
2156+
|| { echo "FAIL: the usage description did not reach the bundle"; cat "$plist"; exit 1; }
2157+
[ "$(pb CFBundleURLTypes:0:CFBundleURLSchemes:0)" = "mcpp-fixture" ] \
2158+
|| { echo "FAIL: the URL type did not reach the bundle"; cat "$plist"; exit 1; }
2159+
[ "$(pb UIDeviceFamily:0)" = "1" ] && ! pb UIDeviceFamily:1 >/dev/null 2>&1 \
2160+
|| { echo "FAIL: UIDeviceFamily is not the project's (1)"; cat "$plist"; exit 1; }
2161+
[ "$(pb CFBundleSupportedPlatforms:0)" = "iPhoneSimulator" ] \
2162+
|| { echo "FAIL: CFBundleSupportedPlatforms is no longer the member's"; cat "$plist"; exit 1; }
2163+
out=$("$MCPP" run --target aarch64-ios-sim --format app 2>&1) && rc=0 || rc=$?
2164+
printf '%s\n' "$out" | tail -20
2165+
[ "$rc" -eq 7 ] \
2166+
|| { echo "FAIL: mcpp run exited $rc with the project's entries, and the program exits 7"; exit 1; }
2167+
echo "ok: the simulator bundle carries the project's entries, and the simulator ran it and returned 7"
2168+
2169+
# NO dist-apk STEP ON THIS HOST, AND THE REASON IS THE ENGINE. The NDK,
2170+
# the build tools, the platform, the JDK, the Kotlin compiler and coursier
2171+
# all publish a macOS table, and `tests/apk-consumer` was packed here once
2172+
# (0.11.0's first run): `mcpp build --target x86_64-linux-android` failed
2173+
# at the application's own link with
2174+
#
2175+
# ld64.lld: error: unknown argument '-soname'
2176+
#
2177+
# before any dist-apk step ran. On a macOS host mcpp 2026.9.14.2 composes
2178+
# the link line in its macOS branch, which states the target triple only
2179+
# for an Apple row, so `-fuse-ld=lld` picks the Mach-O flavour
2180+
# (mcpp-community/mcpp#647, E3). The steps return when an engine links
2181+
# that row; the Windows NDK has no libc++ module surface, so that host
2182+
# has no Android row at all.
2183+
20732184
- name: the rule declared its own compiler
20742185
working-directory: tests/spirv-consumer
20752186
run: |

0 commit comments

Comments
 (0)