From acba3e806f58d9f3866990a819154897d619cd62 Mon Sep 17 00:00:00 2001 From: peachbits Date: Tue, 4 Aug 2026 12:26:01 -0700 Subject: [PATCH] Add size optimizations to the Android build Link with --gc-sections, --icf=safe, --pack-dyn-relocs=android, and --strip-debug, compile every dependency with -ffunction-sections -fdata-sections, trim 36 unused OpenSSL features, and drop lwsf/monero Release builds from -O3 to -O2. The APK-stripped payload shrinks 37.9% on arm64-v8a (15.2 MiB to 9.5 MiB) and 33.5% on armeabi-v7a (12.4 MiB to 8.3 MiB); the npm artifact halves because DWARF is stripped at link time while the symbol table stays for symbolication. Verified on both ABIs: the 4 JNI exports are the only dynamic symbols, all 13 mnemonic wordlists, bulletproof_plus_PROVE, wallet2, lwsf, and the TLS client survive gc-sections, and dead consensus/daemon code (BlockchainLMDB, SM2 tables, miniupnp) is gone. Co-Authored-By: Claude Opus 5 --- CHANGELOG.md | 2 ++ scripts/build-native.ts | 21 ++++++++++++- scripts/libraries/libexpat.ts | 14 ++++++++- scripts/libraries/libsodium.ts | 14 +++++++-- scripts/libraries/libunbound.ts | 18 +++++++++++- scripts/libraries/lwsf.ts | 14 +++++++-- scripts/libraries/openssl.ts | 52 ++++++++++++++++++++++++++++++++- 7 files changed, 125 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7f31309..2298404 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +- changed: Cut the Android native payload by roughly a third with dead-code elimination (--gc-sections against the 4-symbol JNI export list, identical-function folding, packed relocations), a trimmed OpenSSL build (36 unused features removed, QUIC and SM2 tables included), and size-tuned optimization levels. APK-stripped librnmonero.so: arm64-v8a 15.2 MiB to 9.5 MiB, armeabi-v7a 12.4 MiB to 8.3 MiB. The npm artifact also drops DWARF at link time (symbol table kept), halving its size. + ## 0.4.2 (2026-07-17) - changed: Bump vtnerd/lwsf to da8e2617958312f10fe4406808c2a951c5cf0a09 diff --git a/scripts/build-native.ts b/scripts/build-native.ts index d816e09..f37dd6b 100644 --- a/scripts/build-native.ts +++ b/scripts/build-native.ts @@ -103,6 +103,9 @@ const ffi = defineLib({ await build.exec(useCxx ? platform.tools.CXX : platform.tools.CC, [ '-c', ...(useCxx ? ['-std=c++17'] : []), + '-Oz', + '-ffunction-sections', + '-fdata-sections', ...sdkFlags.split(' '), ...includePaths.map(path => `-I${path}`), `-o${object}`, @@ -160,7 +163,23 @@ const ffi = defineLib({ '-llog', `-Wl,--version-script=${join(srcPath, 'jni/exports.map')}`, '-Wl,--no-undefined', - '-Wl,-z,max-page-size=16384' + '-Wl,-z,max-page-size=16384', + + // The version script above exports only 4 JNI symbols, so let the + // linker discard everything unreachable from those (the statically + // linked deps include daemon, consensus, and mining code we never + // call). Requires -ffunction-sections in each library's build: + '-Wl,--gc-sections', + // Fold duplicate identical functions (mostly template instances): + '-Wl,--icf=safe', + // Pack relative relocations (APS2 format, supported since API 23). + // Saves over 1 MiB of .rela.dyn on arm64: + '-Wl,--pack-dyn-relocs=android', + // Keep the symbol table for crash symbolication, but drop DWARF. + // This is what AGP ships in the APK anyway, and it keeps the npm + // package tens of MiB smaller: + '-Wl,--build-id=sha1', + '-Wl,--strip-debug' ]) build.log('done') } diff --git a/scripts/libraries/libexpat.ts b/scripts/libraries/libexpat.ts index 42de20a..aca234c 100644 --- a/scripts/libraries/libexpat.ts +++ b/scripts/libraries/libexpat.ts @@ -4,7 +4,7 @@ import { defineLib } from '../utils/lib' export const libexpat = defineLib({ name: 'libexpat', - cacheTag: '0', + cacheTag: '1', // R_2_7_3: url: 'https://github.com/libexpat/libexpat.git', @@ -16,6 +16,18 @@ export const libexpat = defineLib({ build.exportEnv({ ...platform.tools }) if (platform.type === 'ios') build.exportEnv({ ...platform.sdkFlags }) + // Without an explicit CFLAGS, autoconf defaults to "-g -O2". XML + // parsing is nowhere near a hot path, so optimize for size, and add + // section flags so the final --gc-sections link can drop the parts + // the wallet never calls: + const sizeFlags = '-Oz -g -ffunction-sections -fdata-sections' + build.exportEnv({ + CFLAGS: + platform.type === 'ios' + ? `${platform.sdkFlags.CFLAGS} ${sizeFlags}` + : sizeFlags + }) + await build.exec('./buildconf.sh') await build.exec('./configure', [ '--enable-static', diff --git a/scripts/libraries/libsodium.ts b/scripts/libraries/libsodium.ts index 621049c..33e303e 100644 --- a/scripts/libraries/libsodium.ts +++ b/scripts/libraries/libsodium.ts @@ -2,18 +2,26 @@ import { defineLib } from '../utils/lib' export const libsodium = defineLib({ name: 'libsodium', - cacheTag: '0', + cacheTag: '1', // v1.0.20: url: 'https://github.com/jedisct1/libsodium.git', hash: '9511c982fb1d046470a8b42aa36556cdb7da15de', build: async (build, platform, prefixPath) => { - build.exportEnv({ ...platform.tools }) - build.exportEnv({ ...platform.tools }) if (platform.type === 'ios') build.exportEnv({ ...platform.sdkFlags }) + // Keep -O2 here (this is the crypto hot path), but add section flags + // so the final --gc-sections link can drop unused primitives: + const sizeFlags = '-O2 -g -ffunction-sections -fdata-sections' + build.exportEnv({ + CFLAGS: + platform.type === 'ios' + ? `${platform.sdkFlags.CFLAGS} ${sizeFlags}` + : sizeFlags + }) + await build.exec('./configure', [ '--enable-static', '--disable-shared', diff --git a/scripts/libraries/libunbound.ts b/scripts/libraries/libunbound.ts index b06fb20..bc94ce9 100644 --- a/scripts/libraries/libunbound.ts +++ b/scripts/libraries/libunbound.ts @@ -5,7 +5,7 @@ import { defineLib } from '../utils/lib' export const libunbound = defineLib({ name: 'libunbound', libDeps: ['libexpat', 'openssl'], - cacheTag: '0', + cacheTag: '1', // 1.24.1 (upstream wants 1.4.16) url: 'https://github.com/NLnetLabs/unbound.git', @@ -19,9 +19,25 @@ export const libunbound = defineLib({ }) if (platform.type === 'ios') build.exportEnv({ ...platform.sdkFlags }) + // Without an explicit CFLAGS, autoconf defaults to "-g -O2". DNS + // resolution (OpenAlias) is a cold path, so optimize for size, and + // add section flags so the final --gc-sections link can drop the + // resolver features the wallet never uses: + const sizeFlags = '-Oz -g -ffunction-sections -fdata-sections' + build.exportEnv({ + CFLAGS: + platform.type === 'ios' + ? `${platform.sdkFlags.CFLAGS} ${sizeFlags}` + : sizeFlags + }) + await build.exec('./configure', [ '--enable-static', '--disable-shared', + // GOST verification needs the OpenSSL ENGINE API, which our trimmed + // OpenSSL build removes (no-engine no-gost). No resolver we query + // serves GOST-signed records anyway: + '--disable-gost', `--host=${platform.triple}`, `--prefix=${prefixPath}`, `--with-ssl=${prefixPath}`, diff --git a/scripts/libraries/lwsf.ts b/scripts/libraries/lwsf.ts index a0ae251..9eebc30 100644 --- a/scripts/libraries/lwsf.ts +++ b/scripts/libraries/lwsf.ts @@ -295,7 +295,10 @@ export const lwsf = defineLib({ // Bump this whenever the rpc.cpp / config patch below changes, or the build // silently reuses the cached (unpatched) library. The literal tag does not // hash the patch content, so edits here are invisible to the cache otherwise. - cacheTag: '2-da8e261-txcap', + cacheTag: '3-da8e261-sizeflags', + // Note: nothing zmq ever links into the shipped .so, but monero's + // CMakeLists declares libzmq with pkg_check_modules(REQUIRED), so the + // build must still exist for lwsf's configure step to pass. libDeps: ['boost', 'libsodium', 'libunbound', 'libzmq', 'openssl'], deps: ['monero.clone'], @@ -467,8 +470,13 @@ namespace nymfetch { `-B${join(build.cwd, 'cmake')}`, // Build options: `-DCMAKE_BUILD_TYPE=Release`, - `-DCMAKE_CXX_FLAGS=-DLWSF_MASTER_ENABLE`, - `-DCMAKE_C_FLAGS=-D_DARWIN_C_SOURCE`, + `-DCMAKE_CXX_FLAGS=-DLWSF_MASTER_ENABLE -ffunction-sections -fdata-sections`, + `-DCMAKE_C_FLAGS=-D_DARWIN_C_SOURCE -ffunction-sections -fdata-sections`, + // CMake's default Release level is -O3. -O2 emits noticeably smaller + // code, and the final --gc-sections link can only drop dead functions, + // not shrink the live ones: + `-DCMAKE_C_FLAGS_RELEASE=-O2 -DNDEBUG`, + `-DCMAKE_CXX_FLAGS_RELEASE=-O2 -DNDEBUG`, `-DCMAKE_FIND_ROOT_PATH=${prefixPath};${platform.sysroot}"`, `-DCMAKE_INSTALL_PREFIX=${prefixPath}`, `-DCMAKE_PREFIX_PATH=${prefixPath}`, diff --git a/scripts/libraries/openssl.ts b/scripts/libraries/openssl.ts index 12d6f51..7902c07 100644 --- a/scripts/libraries/openssl.ts +++ b/scripts/libraries/openssl.ts @@ -3,9 +3,52 @@ import { dirname } from 'path' import { defineLib } from '../utils/lib' import type { Platform } from '../utils/platforms' +// Features a TLS client wallet never touches, but which the default build +// links into the shipped binary (QUIC stack, SM2 precompute tables, legacy +// ciphers, CMS/OCSP/TS PKI tooling). Names validated against 3.6.0's +// Configure disablables list. +const sizeConfig = [ + 'no-apps', + 'no-aria', + 'no-blake2', + 'no-camellia', + 'no-cast', + 'no-cmp', + 'no-cms', + 'no-comp', + 'no-ct', + 'no-docs', + 'no-dso', + 'no-dtls', + 'no-ec2m', + 'no-engine', + 'no-gost', + 'no-http', + 'no-idea', + 'no-legacy', + 'no-md4', + 'no-mdc2', + 'no-ocsp', + 'no-psk', + 'no-quic', + 'no-rc2', + 'no-rc4', + 'no-rc5', + 'no-rmd160', + 'no-seed', + 'no-sm2', + 'no-sm3', + 'no-sm4', + 'no-srp', + 'no-srtp', + 'no-tests', + 'no-ts', + 'no-whirlpool' +] + export const openssl = defineLib({ name: 'openssl', - cacheTag: '0', + cacheTag: '1', // 3.6.0 url: 'https://github.com/openssl/openssl.git', @@ -48,6 +91,13 @@ export const openssl = defineLib({ `--prefix=${prefixPath}`, 'no-async', 'no-shared', + ...sizeConfig, + // Configure forwards dash-prefixed args to the compiler. These come + // after its own -O3, so -Oz wins. The hot AES/SHA/EC paths use + // hand-written asm and are unaffected by the C optimization level: + '-Oz', + '-ffunction-sections', + '-fdata-sections', ...extraConfig ]) await build.exec('make', [])