@@ -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
13411362inline 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 ) {
0 commit comments