@@ -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,43 @@ 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. A file that is not
2028+ // an ELF object -- an archive can carry anything under `jni/` -- is copied
2029+ // as it is with a warning, as the Android Gradle plugin packs a library it
2030+ // cannot strip.
2031+ const auto is_elf = [](const std::string& path) {
2032+ std::ifstream in (path, std::ios::binary);
2033+ char magic[4 ] = {};
2034+ return in.read (magic, 4 ) && magic[0 ] == 0x7f && magic[1 ] == ' E' && magic[2 ] == ' L' && magic[3 ] == ' F' ;
2035+ };
2036+ const auto place_library = [&](const std::string& so, const std::string& abi) {
2037+ const fs::path dst = work / " lib" / abi / fs::path (so).filename ();
2038+ if (llvmStrip.empty ()) {
2039+ collect_tree (so, dst, libInputs);
2040+ return ;
2041+ }
2042+ if (!is_elf (so)) {
2043+ mcpp::warning (std::format (
2044+ " mcpp.dist.apk: {} is not an ELF object, so it is packed without being stripped." , so).c_str ());
2045+ collect_tree (so, dst, libInputs);
2046+ return ;
2047+ }
2048+ std::error_code ec;
2049+ fs::create_directories (dst.parent_path (), ec);
2050+ step strip;
2051+ strip.id = std::format (" {}:strip:{}:{}" , bundle ? " aab" : " apk" , abi, fs::path (so).filename ().string ());
2052+ strip.role = " artifact" ;
2053+ strip.description = " LLVM-STRIP " + abi + " /" + fs::path (so).filename ().string ();
2054+ strip.output = dst.string ();
2055+ strip.argv = { llvmStrip, " --strip-unneeded" , " -o" , strip.output , so };
2056+ strip.inputs = { so };
2057+ p.steps .push_back (strip);
2058+ libInputs.push_back (strip.output );
2059+ };
19892060 for (auto const & leg : legs)
19902061 for (auto const & so : leg.libraries )
1991- collect_tree (so, work / " lib " / leg.abi / fs::path (so). filename (), libInputs );
2062+ place_library (so, leg.abi );
19922063
19932064 // An AAR's native libraries, for every ABI this package carries.
19942065 for (auto const & c : contributions) {
@@ -2002,7 +2073,7 @@ inline plan plan_for(options opt = {}) {
20022073 continue ;
20032074 }
20042075 for (auto const & so : shared_objects_in (abiDir))
2005- collect_tree (so, work / " lib " / leg.abi / fs::path (so). filename (), libInputs );
2076+ place_library (so, leg.abi );
20062077 }
20072078 }
20082079
@@ -2049,17 +2120,21 @@ inline plan plan_for(options opt = {}) {
20492120 // dex with `jar`: aapt2 has no flag for native libraries.
20502121 // `--dex <dir>` adds every `classes*.dex` d8 wrote there: a package whose
20512122 // 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.
2123+ // and which of them exist is known only when d8 has run. `--stored <dir>
2124+ // <entry>` adds an entry uncompressed, which native libraries loaded in
2125+ // place must be.
20532126 const std::string copyThenJar = helper (" copy-then-jar.sh" ,
20542127 " #!/bin/sh\n "
20552128 " # mcpp.dist.apk helper. Do not edit.\n "
2056- " # copy-then-jar.sh <src> <dst> <jar> [--dex <dir>] <jar update arguments>...\n "
2129+ " # copy-then-jar.sh <src> <dst> <jar> [--dex <dir>] [--stored <dir> <entry>] <jar update arguments>...\n "
20572130 " set -e\n "
20582131 " src=\" $1\" ; dst=\" $2\" ; jar=\" $3\" ; shift 3\n "
2059- " dex=\"\"\n "
2132+ " dex=\"\" ; stored_dir= \"\" ; stored= \"\" \n"
20602133 " if [ \" ${1:-}\" = --dex ]; then dex=\" $2\" ; shift 2; fi\n "
2134+ " if [ \" ${1:-}\" = --stored ]; then stored_dir=\" $2\" ; stored=\" $3\" ; shift 3; fi\n "
20612135 " cp \" $src\" \" $dst\"\n "
2062- " \" $jar\" uf \" $dst\" \" $@\"\n "
2136+ " if [ -n \" $stored\" ]; then \" $jar\" --update --no-compress --file \" $dst\" -C \" $stored_dir\" \" $stored\" ; fi\n "
2137+ " if [ \" $#\" -gt 0 ]; then \" $jar\" uf \" $dst\" \" $@\" ; fi\n "
20632138 " if [ -n \" $dex\" ]; then (cd \" $dex\" && \" $jar\" uf \" $dst\" classes*.dex); fi\n " );
20642139 const std::string runAndStamp = helper (" run-and-stamp.sh" ,
20652140 " #!/bin/sh\n "
@@ -2408,15 +2483,19 @@ inline plan plan_for(options opt = {}) {
24082483 libs.role = " artifact" ;
24092484 libs.description = " APK LIBS+ASSETS" ;
24102485 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" };
2486+ libs.argv = { copyThenJar, link.output , libs.output , jar };
24142487 if (!javaOutputs.empty ()) {
24152488 // After the three fixed operands and before the `jar` arguments: see
24162489 // `copy-then-jar.sh`.
2417- libs.argv .insert (libs.argv .begin () + 4 , (outDir / " dex" ).string ());
2418- libs.argv .insert (libs.argv .begin () + 4 , " --dex" );
2490+ libs.argv .push_back (" --dex" );
2491+ libs.argv .push_back ((outDir / " dex" ).string ());
2492+ }
2493+ if (inPlaceLibraries) {
2494+ libs.argv .insert (libs.argv .end (), { " --stored" , work.string (), " lib" });
2495+ } else {
2496+ libs.argv .insert (libs.argv .end (), { " -C" , work.string (), " lib" });
24192497 }
2498+ libs.argv .insert (libs.argv .end (), { " -C" , work.string (), " assets" });
24202499 libs.inputs = { link.output };
24212500 for (auto const & f : libInputs) libs.inputs .push_back (f);
24222501 for (auto const & f : assetInputs) libs.inputs .push_back (f);
@@ -2435,7 +2514,12 @@ inline plan plan_for(options opt = {}) {
24352514 // output already exists ("Output file '...' exists"), which every
24362515 // rebuild after the first hits, because ninja does not delete a stale
24372516 // output before an edge reruns it.
2438- align.argv = { zipalign, " -f" , " -p" , " 4" , libs.output , align.output };
2517+ // A library loaded in place is aligned to a 16 KB page, which a 4 KB
2518+ // device reads as well; `apksigner sign` aligns a stored library to the
2519+ // same 16 KB by default, so a signed package keeps it.
2520+ align.argv = inPlaceLibraries
2521+ ? std::vector<std::string>{ zipalign, " -f" , " -P" , " 16" , " 4" , libs.output , align.output }
2522+ : std::vector<std::string>{ zipalign, " -f" , " -p" , " 4" , libs.output , align.output };
24392523 align.inputs = { libs.output };
24402524 p.steps .push_back (align);
24412525 if (!opt.sign ) {
0 commit comments