apply plugin: "com.android.application"
apply plugin: "org.jetbrains.kotlin.android"
apply plugin: "com.facebook.react"
// Native addons the worklet bundle does NOT demand, computed at configure time.
//
// bare-link (which bare-kit runs from its own `preBuild.dependsOn link`) walks the PACKAGE graph and
// stages every dependency whose package.json says `addon: true`. bare-pack resolves the MODULE graph
// reachable from lib/node.js. The difference is real: 61 staged vs 40 demanded on 0.0.1 — 21 libs and
// 50 MB of arm64, including four stale rocksdb-native versions (3.15.0/3.15.2/3.16.0/3.17.1) reached
// only through the symlinked sibling checkouts, each carrying its own installed node_modules.
//
// ⛔ THIS MUST BE A PACKAGING EXCLUDE, NOT A PRUNE. Deleting the extra files before the build does
// nothing — bare-kit's link task restages all four hosts on every build, so a pruned tree produces a
// BYTE-IDENTICAL APK (measured). Packaging is the one point downstream of restaging.
//
// The bundle header is the source of truth, so this cannot go stale as dependencies move: it names
// exactly what bare-kit will dlopen. We only ever exclude libs absent from that list, so this can
// shrink the APK but never break a load. Requires `npm run prebuild:android` first (it packs the
// bundle and populates the addon dir this enumerates).
def computeUndemandedAddons() {
def worklet = file("src/main/assets/worklet.bundle")
def addonsDir = file("../../node_modules/react-native-bare-kit/android/src/main/addons")
if (!worklet.exists()) {
logger.warn("WARNING: no worklet.bundle — run `npm run prebuild:android`. " +
"Packaging every staged addon; the APK will be much larger than it needs to be.")
return []
}
if (!addonsDir.exists()) return []
def raf = new RandomAccessFile(worklet, "r")
try {
// bare-bundle layout: ASCII decimal header length, "\n", then that many bytes of JSON.
def digits = new StringBuilder()
int c
while ((c = raf.read()) != -1 && c != 10) digits.append((char) c)
byte[] header = new byte[digits.toString().trim() as int]
raf.readFully(header)
def parsed = new groovy.json.JsonSlurper().parseText(new String(header, "UTF-8").trim())
def demanded = parsed.addons.collect { it.replaceFirst(/^linked:/, "") } as Set
def excess = [] as Set
addonsDir.eachDir { abi ->
abi.eachFileMatch(~/.*\.so/) { so ->
if (!demanded.contains(so.name)) excess << so.name
}
}
logger.lifecycle("addons: ${demanded.size()} demanded by bundle, ${excess.size()} excluded")
return excess.toList().sort()
} finally {
raf.close()
}
}
// ⛔ PREFER THE KEEP-LIST. computeUndemandedAddons() below reads the bundle header, which names only
// what the JS MODULE graph imports. A demanded addon can LINK against another .so that no JS module
// imports — libbare-fs.4.7.3.so needs libbare-buffer.3.6.1.so — and excluding it produced a load
// failure that surfaced as ADDON_NOT_FOUND (resolution succeeded; dlopen did not). scripts/stage-addons.mjs
// computes the transitive ELF NEEDED closure and writes addon-keep.json; when it is present it wins.
// Its absence falls back to the header-only computation, which is better than nothing but is the thing
// that broke pairing.
def keepFile = file("addon-keep.json")
if (keepFile.exists()) {
def keep = new groovy.json.JsonSlurper().parseText(keepFile.text)
def addonsDir = file("../../node_modules/react-native-bare-kit/android/src/main/addons")
def excess = [] as Set
if (addonsDir.exists()) {
addonsDir.eachDir { abi ->
def keepSet = (keep[abi.name] ?: []) as Set
abi.eachFileMatch(~/.*\.so/) { so -> if (!keepSet.contains(so.name)) excess << so.name }
}
}
logger.lifecycle("addons: keep-list ${keep.values().collect { it.size() }.max() ?: 0} kept, ${excess.size()} excluded (native closure included)")
ext.undemandedAddons = excess.toList().sort()
} else {
ext.undemandedAddons = computeUndemandedAddons()
}
react {
// Metro bundles app/index.js; bare-pack separately bundles the worklet into app/worklet.bundle,
// which Metro then picks up as an asset (.bundle is an assetExt) and ships as res/*.bundle.
// TWO BUNDLERS, TWO ARTIFACTS — see spec/architecture.md §6.2. They are not interchangeable and
// only the worklet bundle is covered by pgh-runtime OTA.
root = file("../../")
entryFile = file("../../app/index.js")
autolinkLibrariesWithApp()
}
android {
namespace "to.favela.android"
compileSdk rootProject.ext.compileSdkVersion
ndkVersion rootProject.ext.ndkVersion
defaultConfig {
applicationId "to.favela.android"
minSdk rootProject.ext.minSdkVersion
targetSdk rootProject.ext.targetSdkVersion
versionCode 2
versionName "0.0.2"
ndk {
// ⛔ ARM64 ONLY. An APK is a self-contained sideloadable ZIP with no notion of "the arch I
// need" — it carries every ABI under lib/ and the installer picks one at install time. The
// splitting lives in the STORE (Play generates per-device APKs from an AAB), and we
// distribute a direct .apk off the forge, so nothing splits for us. Without this filter the
// build packages all four: measured on 0.0.1, 475.8 MB of a 527 MB APK was native libs, of
// which ~351 MB was armeabi-v7a + x86 + x86_64. Every user downloads and stores all four to
// run one.
//
// Nothing else is worth carrying: minSdk is 31, so a 32-bit-only armeabi-v7a device is
// effectively nonexistent, and x86/x86_64 are emulators and Chromebooks, not phones. To
// build for an emulator (spike 3/5), override on the command line rather than editing this:
// ./gradlew assembleRelease -PreactNativeArchitectures=x86_64 (and drop this filter)
abiFilters "arm64-v8a"
}
}
signingConfigs {
// ⛔ A DURABLE KEY, NOT A PER-BUILD DEBUG KEY. Android refuses to UPGRADE an APK signed with a
// different key; the only way out is uninstall, which deletes app-private storage — i.e. the
// node's identity, its device chain and its repo. A throwaway debug key would make every
// rebuild a data-loss event. Generated once by build.sh and kept.
release {
storeFile file(System.getenv("FAVELA_KEYSTORE") ?: "${System.getenv('HOME')}/android-toolchain/favela-spike.keystore")
storePassword System.getenv("FAVELA_KEYSTORE_PASS") ?: "favela"
keyAlias System.getenv("FAVELA_KEY_ALIAS") ?: "favela"
keyPassword System.getenv("FAVELA_KEY_PASS") ?: "favela"
}
}
buildTypes {
debug { signingConfig signingConfigs.release }
release {
signingConfig signingConfigs.release
minifyEnabled false
}
}
packagingOptions {
// bare-kit and our addons both ship libc++_shared; first one wins rather than failing the build.
pickFirst "**/libc++_shared.so"
jniLibs {
excludes += undemandedAddons.collect { "**/" + it }
}
}
}
dependencies {
implementation("com.facebook.react:react-android")
implementation("com.facebook.react:hermes-android")
}