OverviewFilesCommitsIssuesDocsReleasesPeers

favela-android / android / app / build.gradle

155 lines · gradle ref:
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")
}