Skip to content

Commit fcbe4e1

Browse files
committed
0.11.1: dist-apk packs native libraries as the Android Gradle plugin does; dist-appimage takes an SVG icon
A HuxerUI application packed by dist-apk and by Gradle differed in its native libraries: dist-apk shipped them with their symbol tables and debug information (libhuxerui.so 8.1 MB against 5.5 MB, the NDK's libc++_shared.so 9.1 MB against 1.3 MB), and always compressed, so a manifest stating android:extractNativeLibs="false" produced a package the platform refuses. Each native library is now written by an `llvm-strip --strip-unneeded` action with the build's own tool, beside the compiler mcpp::toolchain_dir() reports; options::keep_debug_symbols packs them as staged. A manifest stating extractNativeLibs="false" gets lib/ stored uncompressed and aligned to 16 KB pages; one stating nothing is packed compressed as before. dist-appimage's options::icon may be an SVG: the staged icon keeps the supplied extension, a stale file of the other format is removed, any other format is refused through mcpp::warning, and the payload floor ignores the SVG. Tests: check-apk-features.sh (k) stripped, compressed by default, stored and 16 KB-aligned when loaded in place; (l) keep_debug_symbols. appimage-consumer packs an SVG icon and refuses a .jpg.
1 parent 56c0bd6 commit fcbe4e1

11 files changed

Lines changed: 217 additions & 22 deletions

File tree

.github/workflows/ci.yml

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1164,6 +1164,29 @@ jobs:
11641164
fi
11651165
echo "ok: one AppImage, and it printed the program's output"
11661166
1167+
# AN SVG ICON KEEPS ITS FORMAT (0.11.1): the desktop entry names the
1168+
# icon without an extension and appimagetool finds it by the file's,
1169+
# so the image carries `<name>.svg` with the supplied bytes and no
1170+
# PNG of that name. A format no desktop entry reads is refused.
1171+
APPIMAGE_CONSUMER_ICON="$PWD/assets/icon.svg" "$MCPP" pack --format appimage | tee pack-svg.log
1172+
img=$(find target -name '*.AppImage' | head -1)
1173+
rm -rf squashfs-root
1174+
APPIMAGE_EXTRACT_AND_RUN=1 "$img" --appimage-extract > /dev/null
1175+
cmp assets/icon.svg squashfs-root/AppimageConsumer.svg \
1176+
|| { echo "FAIL: the image does not carry the SVG icon"; ls -la squashfs-root; exit 1; }
1177+
test ! -e squashfs-root/AppimageConsumer.png \
1178+
|| { echo "FAIL: the placeholder PNG is still beside the SVG"; exit 1; }
1179+
grep -qx 'Icon=AppimageConsumer' squashfs-root/AppimageConsumer.desktop \
1180+
|| { echo "FAIL: the desktop entry does not name the icon"; exit 1; }
1181+
cp assets/icon.svg assets/icon.jpg
1182+
set +e
1183+
APPIMAGE_CONSUMER_ICON="$PWD/assets/icon.jpg" "$MCPP" pack --format appimage > pack-jpg.log 2>&1
1184+
set -e
1185+
grep -q 'neither a .png nor an .svg' pack-jpg.log \
1186+
|| { cat pack-jpg.log; echo "FAIL: a .jpg icon was not refused by name"; exit 1; }
1187+
rm -f assets/icon.jpg
1188+
echo "ok: an SVG icon is packed as an SVG, and a .jpg is refused"
1189+
11671190
# THE iOS ROW OF `dist-apple`, AT THE PLAN LEVEL (#622 B1). No runner
11681191
# in this workflow has an Apple SDK, so `--target aarch64-ios-sim`
11691192
# refuses before `build.mcpp` even runs -- see

README.md

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

dist/apk.cppm

Lines changed: 82 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,14 @@ struct options {
215215
// things.
216216
bool sign = true;
217217

218+
// `true` packs the native libraries as the engine staged them (0.11.1).
219+
// By default each is stripped with the build's own `llvm-strip
220+
// --strip-unneeded`, which keeps the dynamic symbols the loader reads and
221+
// drops the symbol table and the debug information -- what the Android
222+
// Gradle plugin does to every library it packages, and what `mcpp pack`
223+
// reports it did.
224+
bool keep_debug_symbols = false;
225+
218226
// LEVEL 1, KOTLIN (0.11.0). One or more directories of `.kt` sources,
219227
// compiled by `kotlinc` with every Java root as its reference sources,
220228
// before `javac` compiles the Java against the Kotlin classes; the Kotlin
@@ -1336,6 +1344,19 @@ inline std::vector<std::string> shared_objects_in(const fs::path& dir) {
13361344
return out;
13371345
}
13381346

1347+
// Does the manifest ask for the native libraries to be loaded from the APK in
1348+
// place (`<application android:extractNativeLibs="false">`, 0.11.1)? The
1349+
// platform can map a library only from an entry stored uncompressed on a page
1350+
// boundary, so such a package is laid out that way; a compressed library in it
1351+
// fails to install.
1352+
inline bool loads_native_libraries_in_place(const std::string& manifest) {
1353+
xml::node root;
1354+
std::string error;
1355+
if (!xml::parse(manifest, root, error) || root.name != "manifest") return false;
1356+
const xml::node* application = find_child(root, "application");
1357+
return application && xml::attr_of(*application, "android:extractNativeLibs") == "false";
1358+
}
1359+
13391360
// ─── Plan ──────────────────────────────────────────────────────────────────
13401361

13411362
inline plan plan_for(options opt = {}) {
@@ -1975,6 +1996,22 @@ inline plan plan_for(options opt = {}) {
19751996
return refuse(p, "cannot write AndroidManifest.xml", std::format(
19761997
"mcpp.dist.apk: cannot write {}.", manifestPath));
19771998
}
1999+
const bool inPlaceLibraries = loads_native_libraries_in_place(manifestBytes);
2000+
2001+
// THE BUILD'S OWN llvm-strip (0.11.1): the one beside the compiler mcpp
2002+
// resolved for this row, the NDK's.
2003+
std::string llvmStrip;
2004+
if (!opt.keep_debug_symbols) {
2005+
const std::string toolchain = mcpp::toolchain_dir();
2006+
const fs::path candidate = fs::path(toolchain) / "bin" / "llvm-strip";
2007+
if (!toolchain.empty() && is_file(candidate.string())) {
2008+
llvmStrip = candidate.string();
2009+
} else {
2010+
mcpp::warning(std::format(
2011+
"mcpp.dist.apk: no llvm-strip beside the toolchain ({}); the native libraries are packed "
2012+
"with their debug information.", toolchain.empty() ? "none reported" : toolchain).c_str());
2013+
}
2014+
}
19782015

19792016
// ── the temporary staging tree: lib/<abi>/, assets/ ─────────────────
19802017
//
@@ -1986,9 +2023,29 @@ inline plan plan_for(options opt = {}) {
19862023
const fs::path work = outDir / "stage";
19872024
{ std::error_code ec; fs::remove_all(work, ec); }
19882025
std::vector<std::string> libInputs;
2026+
// A library reaches `lib/<abi>/` stripped, as an action of its own, or
2027+
// copied as it is when the debug information is kept.
2028+
const auto place_library = [&](const std::string& so, const std::string& abi) {
2029+
const fs::path dst = work / "lib" / abi / fs::path(so).filename();
2030+
if (llvmStrip.empty()) {
2031+
collect_tree(so, dst, libInputs);
2032+
return;
2033+
}
2034+
std::error_code ec;
2035+
fs::create_directories(dst.parent_path(), ec);
2036+
step strip;
2037+
strip.id = std::format("{}:strip:{}:{}", bundle ? "aab" : "apk", abi, fs::path(so).filename().string());
2038+
strip.role = "artifact";
2039+
strip.description = "LLVM-STRIP " + abi + "/" + fs::path(so).filename().string();
2040+
strip.output = dst.string();
2041+
strip.argv = { llvmStrip, "--strip-unneeded", "-o", strip.output, so };
2042+
strip.inputs = { so };
2043+
p.steps.push_back(strip);
2044+
libInputs.push_back(strip.output);
2045+
};
19892046
for (auto const& leg : legs)
19902047
for (auto const& so : leg.libraries)
1991-
collect_tree(so, work / "lib" / leg.abi / fs::path(so).filename(), libInputs);
2048+
place_library(so, leg.abi);
19922049

19932050
// An AAR's native libraries, for every ABI this package carries.
19942051
for (auto const& c : contributions) {
@@ -2002,7 +2059,7 @@ inline plan plan_for(options opt = {}) {
20022059
continue;
20032060
}
20042061
for (auto const& so : shared_objects_in(abiDir))
2005-
collect_tree(so, work / "lib" / leg.abi / fs::path(so).filename(), libInputs);
2062+
place_library(so, leg.abi);
20062063
}
20072064
}
20082065

@@ -2049,17 +2106,21 @@ inline plan plan_for(options opt = {}) {
20492106
// dex with `jar`: aapt2 has no flag for native libraries.
20502107
// `--dex <dir>` adds every `classes*.dex` d8 wrote there: a package whose
20512108
// classes pass the 64K-method limit of one dex gets `classes2.dex` and on,
2052-
// and which of them exist is known only when d8 has run.
2109+
// and which of them exist is known only when d8 has run. `--stored <dir>
2110+
// <entry>` adds an entry uncompressed, which native libraries loaded in
2111+
// place must be.
20532112
const std::string copyThenJar = helper("copy-then-jar.sh",
20542113
"#!/bin/sh\n"
20552114
"# mcpp.dist.apk helper. Do not edit.\n"
2056-
"# copy-then-jar.sh <src> <dst> <jar> [--dex <dir>] <jar update arguments>...\n"
2115+
"# copy-then-jar.sh <src> <dst> <jar> [--dex <dir>] [--stored <dir> <entry>] <jar update arguments>...\n"
20572116
"set -e\n"
20582117
"src=\"$1\"; dst=\"$2\"; jar=\"$3\"; shift 3\n"
2059-
"dex=\"\"\n"
2118+
"dex=\"\"; stored_dir=\"\"; stored=\"\"\n"
20602119
"if [ \"${1:-}\" = --dex ]; then dex=\"$2\"; shift 2; fi\n"
2120+
"if [ \"${1:-}\" = --stored ]; then stored_dir=\"$2\"; stored=\"$3\"; shift 3; fi\n"
20612121
"cp \"$src\" \"$dst\"\n"
2062-
"\"$jar\" uf \"$dst\" \"$@\"\n"
2122+
"if [ -n \"$stored\" ]; then \"$jar\" --update --no-compress --file \"$dst\" -C \"$stored_dir\" \"$stored\"; fi\n"
2123+
"if [ \"$#\" -gt 0 ]; then \"$jar\" uf \"$dst\" \"$@\"; fi\n"
20632124
"if [ -n \"$dex\" ]; then (cd \"$dex\" && \"$jar\" uf \"$dst\" classes*.dex); fi\n");
20642125
const std::string runAndStamp = helper("run-and-stamp.sh",
20652126
"#!/bin/sh\n"
@@ -2408,15 +2469,19 @@ inline plan plan_for(options opt = {}) {
24082469
libs.role = "artifact";
24092470
libs.description = "APK LIBS+ASSETS";
24102471
libs.output = (outDir / "withlibs.apk").string();
2411-
libs.argv = { copyThenJar, link.output, libs.output, jar,
2412-
"-C", work.string(), "lib",
2413-
"-C", work.string(), "assets" };
2472+
libs.argv = { copyThenJar, link.output, libs.output, jar };
24142473
if (!javaOutputs.empty()) {
24152474
// After the three fixed operands and before the `jar` arguments: see
24162475
// `copy-then-jar.sh`.
2417-
libs.argv.insert(libs.argv.begin() + 4, (outDir / "dex").string());
2418-
libs.argv.insert(libs.argv.begin() + 4, "--dex");
2476+
libs.argv.push_back("--dex");
2477+
libs.argv.push_back((outDir / "dex").string());
2478+
}
2479+
if (inPlaceLibraries) {
2480+
libs.argv.insert(libs.argv.end(), { "--stored", work.string(), "lib" });
2481+
} else {
2482+
libs.argv.insert(libs.argv.end(), { "-C", work.string(), "lib" });
24192483
}
2484+
libs.argv.insert(libs.argv.end(), { "-C", work.string(), "assets" });
24202485
libs.inputs = { link.output };
24212486
for (auto const& f : libInputs) libs.inputs.push_back(f);
24222487
for (auto const& f : assetInputs) libs.inputs.push_back(f);
@@ -2435,7 +2500,12 @@ inline plan plan_for(options opt = {}) {
24352500
// output already exists ("Output file '...' exists"), which every
24362501
// rebuild after the first hits, because ninja does not delete a stale
24372502
// output before an edge reruns it.
2438-
align.argv = { zipalign, "-f", "-p", "4", libs.output, align.output };
2503+
// A library loaded in place is aligned to a 16 KB page, which a 4 KB
2504+
// device reads as well; `apksigner sign` aligns a stored library to the
2505+
// same 16 KB by default, so a signed package keeps it.
2506+
align.argv = inPlaceLibraries
2507+
? std::vector<std::string>{ zipalign, "-f", "-P", "16", "4", libs.output, align.output }
2508+
: std::vector<std::string>{ zipalign, "-f", "-p", "4", libs.output, align.output };
24392509
align.inputs = { libs.output };
24402510
p.steps.push_back(align);
24412511
if (!opt.sign) {

dist/appimage.cppm

Lines changed: 31 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -80,10 +80,14 @@ struct options {
8080
// here is visible only to a desktop launcher, never to a build.
8181
bool terminal = true;
8282

83-
// A PNG a project supplies. Empty uses the built-in placeholder, which
84-
// exists so that an AppImage can be produced with nothing declared:
85-
// appimagetool requires an icon and refuses without one, and a member whose
86-
// first use needs a graphic asset is a member nobody tries.
83+
// A PNG or, from 0.11.1, an SVG a project supplies. The desktop entry
84+
// names the icon without an extension and a reader -- appimagetool, a
85+
// desktop's icon lookup -- finds it by the file's, so the file keeps the
86+
// one it was supplied with; any other is refused. Empty uses the built-in
87+
// placeholder, which exists so that an AppImage can be produced with
88+
// nothing declared: appimagetool requires an icon and refuses without one,
89+
// and a member whose first use needs a graphic asset is a member nobody
90+
// tries.
8791
std::string icon;
8892

8993
// An explicit `appimagetool` wins over discovery. Set it to pin a build
@@ -365,12 +369,33 @@ inline plan plan_for(options opt = {}) {
365369
const auto launcher_rel = mcpp::plugins::names::relative_to(launcher, stage);
366370

367371
// ── The three files AppImage requires, written into the staged tree ────
372+
std::string iconExtension = ".png";
373+
if (!opt.icon.empty()) {
374+
iconExtension = std::filesystem::path(opt.icon).extension().string();
375+
for (char& c : iconExtension) c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
376+
if (iconExtension != ".png" && iconExtension != ".svg") {
377+
// Also a warning: the engine discards a build program's stderr when it exits 0.
378+
const std::string message = std::format(
379+
"mcpp.dist.appimage: the icon {} is neither a .png nor an .svg, the two "
380+
"formats a desktop entry's icon is read in.", opt.icon);
381+
std::cerr << message << '\n';
382+
mcpp::warning(message.c_str());
383+
p.reason = "icon format";
384+
return p;
385+
}
386+
}
368387
const std::string name = app_name_for(opt);
369388
const auto stagePath = std::filesystem::path(stage);
370389
const auto desktop = stagePath / (name + ".desktop");
371-
const auto icon = stagePath / (name + ".png");
390+
const auto icon = stagePath / (name + iconExtension);
372391
const auto dirIcon = stagePath / ".DirIcon";
373392
const auto runFile = stagePath / "AppRun";
393+
// The other format's file from an earlier pack of this tree: appimagetool
394+
// takes a PNG over an SVG of the same name.
395+
{
396+
std::error_code ec;
397+
std::filesystem::remove(stagePath / (name + (iconExtension == ".png" ? ".svg" : ".png")), ec);
398+
}
374399

375400
std::string iconBytes;
376401
if (!opt.icon.empty()) {
@@ -481,7 +506,7 @@ inline bool submit(const plan& p) {
481506
const auto name = e.path().filename().string();
482507
if (e.path().parent_path() == std::filesystem::path(p.appdir)
483508
&& (name == "AppRun" || name == ".DirIcon"
484-
|| name.ends_with(".desktop") || name.ends_with(".png")))
509+
|| name.ends_with(".desktop") || name.ends_with(".png") || name.ends_with(".svg")))
485510
continue;
486511
++carried;
487512
}

mcpp.toml

Lines changed: 1 addition & 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.11.0"
4+
version = "0.11.1"
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"]

src/plugins.cppm

Lines changed: 1 addition & 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.11.0";
52+
inline constexpr std::string_view version = "0.11.1";
5353

5454
} // namespace mcpp::plugins
5555

tests/apk-consumer/build.mcpp

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,10 @@ int main() {
7272
opt.sign = false;
7373
opt.keystore = "xim:android-debug-keystore";
7474
}
75+
// (l) the native libraries as the engine staged them, symbol table and all.
76+
if (const char* keep = std::getenv("APK_CONSUMER_KEEP_DEBUG_SYMBOLS"); keep && *keep) {
77+
opt.keep_debug_symbols = true;
78+
}
7579

7680
return mcpp::dist::apk::generate(opt) ? 0 : 1;
7781
}

tests/apk-consumer/check-apk-features.sh

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -258,3 +258,52 @@ export APK_CONSUMER_SIGN_CONFLICT=1
258258
grep -q 'opposite things' pack-j.log || fail "the refusal does not say the two options contradict" pack-j.log
259259
unset APK_CONSUMER_SIGN_CONFLICT
260260
echo "ok: sign = false with a keystore is refused"
261+
262+
# ── (k),(l) 0.11.1: native libraries as the Android Gradle plugin packs them ─
263+
#
264+
# (k) A packed library is stripped with the build's own llvm-strip
265+
# (`--strip-unneeded`: no symbol table and no debug information, the dynamic
266+
# symbols kept), and a package whose manifest states nothing stores it
267+
# compressed, as before. A manifest stating `android:extractNativeLibs="false"`
268+
# gets it stored uncompressed and aligned to a 16 KB page, which the platform
269+
# needs to load it from the APK in place. (l) `keep_debug_symbols` packs the
270+
# library as the engine staged it.
271+
LIB=lib/x86_64/libapk-consumer.so
272+
method_of() { unzip -v "$1" | awk -v name="$LIB" '$NF == name { print $2 }'; }
273+
274+
echo "== (k) a stripped library, stored and 16 KB-aligned when loaded in place =="
275+
rm -rf target k
276+
unset APK_CONSUMER_TEMPLATE APK_CONSUMER_KEEP_DEBUG_SYMBOLS || true
277+
"$MCPP" pack --format apk --target "$TARGET" > pack-k1.log 2>&1 || fail "pack failed" pack-k1.log
278+
APK=$(find target -name 'apk-consumer.apk' | head -1)
279+
[ -n "$APK" ] || fail "no apk-consumer.apk" pack-k1.log
280+
mkdir -p k && unzip -q -o "$APK" "$LIB" -d k
281+
readelf -S "k/$LIB" > sections-k1.log
282+
if grep -qE '\.symtab|\.debug_' sections-k1.log; then fail "the packed library keeps its symbol table or debug information" sections-k1.log; fi
283+
readelf --dyn-syms -W "k/$LIB" > dynsym-k1.log
284+
grep -q 'ANativeActivity_onCreate' dynsym-k1.log || fail "stripping dropped the entry point the platform calls" dynsym-k1.log
285+
[ "$(method_of "$APK")" != Stored ] || fail "a manifest stating nothing got its library stored uncompressed" pack-k1.log
286+
287+
rm -rf target
288+
export APK_CONSUMER_TEMPLATE=manifest-template-in-place.xml
289+
"$MCPP" pack --format apk --target "$TARGET" > pack-k2.log 2>&1 || fail "pack failed" pack-k2.log
290+
APK=$(find target -name 'apk-consumer.apk' | head -1)
291+
[ -n "$APK" ] || fail "no apk-consumer.apk" pack-k2.log
292+
[ "$(method_of "$APK")" = Stored ] || fail "a library loaded in place is stored compressed" pack-k2.log
293+
"$BT/zipalign" -c -P 16 4 "$APK" > align-k2.log 2>&1 || fail "the library is not aligned to a 16 KB page" align-k2.log
294+
"$AAPT2" dump xmltree "$APK" --file AndroidManifest.xml > xmltree-k2.log 2>&1
295+
grep -q 'extractNativeLibs.*=false' xmltree-k2.log || fail "the manifest does not state extractNativeLibs=false" xmltree-k2.log
296+
unset APK_CONSUMER_TEMPLATE
297+
echo "ok: the library is stripped, compressed by default, and stored on a 16 KB page when loaded in place"
298+
299+
echo "== (l) keep_debug_symbols =="
300+
rm -rf target k
301+
export APK_CONSUMER_KEEP_DEBUG_SYMBOLS=1
302+
"$MCPP" pack --format apk --target "$TARGET" > pack-l.log 2>&1 || fail "pack failed" pack-l.log
303+
APK=$(find target -name 'apk-consumer.apk' | head -1)
304+
[ -n "$APK" ] || fail "no apk-consumer.apk" pack-l.log
305+
mkdir -p k && unzip -q -o "$APK" "$LIB" -d k
306+
readelf -S "k/$LIB" > sections-l.log
307+
grep -q '\.symtab' sections-l.log || fail "keep_debug_symbols packed a stripped library" sections-l.log
308+
unset APK_CONSUMER_KEEP_DEBUG_SYMBOLS
309+
echo "ok: keep_debug_symbols packs the library with its symbol table"
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<!-- Fixture: a project manifest that asks for the native libraries to be
3+
loaded from the APK in place, as the Android Gradle plugin's manifest
4+
does from minSdk 23; criterion (k) checks the libraries are then stored
5+
uncompressed on a 16 KB page. -->
6+
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
7+
android:versionName="{{version_name}}" android:versionCode="{{version_code}}"
8+
package="{{application_id}}">
9+
<uses-sdk android:minSdkVersion="{{min_sdk}}" android:targetSdkVersion="{{target_sdk}}"/>
10+
<application android:label="{{label}}" android:hasCode="false" android:extractNativeLibs="false">
11+
<activity android:name="{{activity}}" android:exported="true">
12+
<meta-data android:name="android.app.lib_name" android:value="{{lib_name}}"/>
13+
<intent-filter>
14+
<action android:name="android.intent.action.MAIN"/>
15+
<category android:name="android.intent.category.LAUNCHER"/>
16+
</intent-filter>
17+
</activity>
18+
</application>
19+
</manifest>
Lines changed: 1 addition & 0 deletions
Loading

0 commit comments

Comments
 (0)