// Inside the android { } block the name `java` resolves to Gradle's own // java extension and shadows the package, so these types have to be // imported here rather than fully qualified at the use site. import java.util.Properties plugins { alias(libs.plugins.android.application) alias(libs.plugins.kotlin.android) alias(libs.plugins.kotlin.compose) } android { // ARM64-only-device prototype (see /ARM64_TRANSLATION_LAYER.md): "_arm" // instead of "_mod" so this build can be installed side by side with // the real armeabi-v7a mod, not overwrite it. namespace = "com.ea.games.nfs13_arm" compileSdk = 35 defaultConfig { applicationId = "com.ea.games.nfs13_arm" minSdk = 27 targetSdk = 35 versionCode = 1003128 versionName = "1.3.128" // mpcore is now plain native ARM64 code driving an embedded ARM32 // CPU-emulation core - no armeabi-v7a native code is built/shipped // for this app at all. ndk.abiFilters.add("arm64-v8a") // x86_64 (2026-09-22): lets the translated build install in WayDroid, // whose Android image is x86_64. Unicorn picks its tcg/i386 backend // there automatically, so the ARM32 guest is translated by US rather // than by houdini - which is what made earlier WayDroid attempts with // the native ARM build unreliable. // // NOTE: the flat guest mapping (task #61, worth 1.24x) is implemented // only in tcg/aarch64, so an x86_64 host silently falls back to the // software MMU and will be slower. Fine for a test target; port the // same change into tcg/i386 if it ever becomes a shipping target. ndk.abiFilters.add("x86_64") // Default: false. Overridden to true by the "native32" flavor - see // its own comment below. buildConfigField("boolean", "NATIVE32", "false") testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } ndkVersion = "27.0.12077973" // Release signing. The keystore and its password live OUTSIDE this repo // (~/keystores/), deliberately: this is a git working tree, and a signing // identity that can be committed eventually is committed. Gradle reads // them from a properties file if it is there, and falls back to the debug // key if it is not - so a checkout without the key still builds, it just // produces something that cannot be shipped. // // To create the key (once, and then back it up - losing it means never // being able to update an installed build): // // keytool -genkeypair -v -keystore ~/keystores/nfsmw-release.jks \ // -alias nfsmw -keyalg RSA -keysize 4096 -validity 10950 // // then write ~/keystores/nfsmw-release.properties with: // storeFile=/home//keystores/nfsmw-release.jks // storePassword=... // keyAlias=nfsmw // keyPassword=... signingConfigs { val releaseProps = File(System.getProperty("user.home"), "keystores/nfsmw-release.properties") if (releaseProps.isFile) { val props = Properties().apply { releaseProps.inputStream().use { load(it) } } create("release") { storeFile = file(props.getProperty("storeFile")) storePassword = props.getProperty("storePassword") keyAlias = props.getProperty("keyAlias") keyPassword = props.getProperty("keyPassword") // Both modern signature schemes: v2 covers the whole APK and // is what Android 7+ verifies, v3 allows rotating the key // later without invalidating existing installs. enableV2Signing = true enableV3Signing = true } } } buildTypes { release { isMinifyEnabled = false proguardFiles( getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro" ) // Local-testing-only (2026-09-05, "why is this ~100x slower than // native" investigation - see ARM64_TRANSLATION_LAYER.md): no // real release signing config exists in this prototype, and // there's no intent to ship this build anywhere - reusing the // auto-generated debug keystore just makes `assembleTranslatedRelease` // installable on a test device the same way translatedDebug // already is. The actual point of testing this build type is the // NATIVE side (mpcore's CMakeLists.txt now forces // CMAKE_BUILD_TYPE=RelWithDebInfo unconditionally, so this // isn't even required for that fix to take effect - it's here so // this variant can be installed at all for a genuine debuggable=false // comparison, e.g. CheckJNI's behavior). // Real key when it exists, debug key otherwise. Announced at // configuration time rather than discovered later from a // mysteriously unshippable APK. signingConfig = signingConfigs.findByName("release") ?: run { logger.warn( "RELEASE SIGNING: ~/keystores/nfsmw-release.properties not found - " + "signing with the DEBUG key. This APK must NOT be distributed." ) signingConfigs.getByName("debug") } } } compileOptions { sourceCompatibility = JavaVersion.VERSION_11 targetCompatibility = JavaVersion.VERSION_11 } kotlinOptions { jvmTarget = "11" } androidResources { // The bundled game data (assets/game_data.obb, ~595 MB) is an already // compressed archive - deflating it again would cost a long build and // a slower install for no size gain, and it must stay STORED so it can // be streamed straight out of the APK. noCompress += "obb" } buildFeatures { compose = true buildConfig = true } // "native32" flavor - diagnostic-only reference build for the native // ARM32 tracing investigation (see ARM64_TRANSLATION_LAYER.md / // sorted-popping-plum.md's "Native ARM32 tracing harness" plan). Real // armeabi-v7a support and a BuildConfig.NATIVE32 flag GameActivityMain // uses to load the real, unmodified native libraries directly // (System.loadLibrary) instead of going through mpcore's emulation path // - only meaningful on genuinely 32-bit-capable hardware (the Galaxy // A9), never intended to ship. MUST be armeabi-v7a-ONLY (not just // "armeabi-v7a added alongside arm64-v8a") - confirmed live on-device // that shipping both ABIs makes Android launch the process via the // 64-bit app_process64/Zygote (arm64-v8a is present, so it's preferred), // which can never dlopen/LD_PRELOAD a 32-bit .so at all (a process's // bitness is fixed for its whole lifetime, not per-library) - defeats // the entire point of this flavor. ndk.abiFilters.clear() first, // since AGP's per-flavor abiFilters otherwise ADD to defaultConfig's // set rather than replace it. flavorDimensions += "abi" productFlavors { create("translated") { dimension = "abi" // Default/existing behavior - the emulated arm64-v8a path, // unchanged from before this flavor split existed. } create("native32") { dimension = "abi" ndk.abiFilters.clear() ndk.abiFilters.add("armeabi-v7a") buildConfigField("boolean", "NATIVE32", "true") } } // 2026-09-16 (ARM64_TRANSLATION_LAYER.md - trace_agent wrap.sh deployment): // manually injecting a bundled wrap.sh into the packaged native32 APK // (AGP's own native-lib merge/strip pipeline only recognizes *.so files // and silently drops anything else) failed to install at all with // "Failed to extract native libraries, res=-2" under the default // extractNativeLibs=false packaging - PackageManager validates every // entry under lib// as a loadable library when it plans to mmap // straight from the APK, and a plain shell script fails that check. // Legacy (extracted-to-disk) packaging doesn't do that same strict // validation. Only applied to native32 - the translated flavor doesn't // need or want this (slightly larger install, marginally slower first // native-lib load). productFlavors.getByName("native32") { packaging { jniLibs { useLegacyPackaging = true } } } } // 2026-09-06 (native32-on-real-hardware launch investigation - see // ARM64_TRANSLATION_LAYER.md): ndk.abiFilters above only restricts THIS // module's own native code - the "native32" flavor still needs `mpcore` on // the compile classpath (GameActivityMain.kt's shared, non-flavor-specific // source calls mpcore functions in its non-NATIVE32 branch), and mpcore's // own CMakeLists always builds arm64-v8a regardless of which app flavor // pulls it in. That arm64-v8a .so was silently merged into the native32 // APK too, so a real device with both an arm64-v8a AND an armeabi-v7a lib // present chose to launch the process as 64-bit (app_process64) - which can // never load this flavor's 32-bit fmodex/fmodevent/Nimble/app libraries at // all, confirmed live via `UnsatisfiedLinkError: couldn't find "libfmodex.so"` // (nativeLibraryDirectories only listed arm64/arm64-v8a paths). Explicitly // drop every arm64-v8a .so from the native32 variant's packaging so the // APK is genuinely armeabi-v7a-only, restoring 32-bit process selection. androidComponents { onVariants(selector().withFlavor("abi" to "native32")) { variant -> variant.packaging.jniLibs.excludes.add("lib/arm64-v8a/*.so") } } dependencies { implementation(libs.androidx.core.ktx) implementation(libs.androidx.appcompat) implementation(libs.material) testImplementation(libs.junit) androidTestImplementation(libs.androidx.junit) androidTestImplementation(libs.androidx.espresso.core) implementation(platform(libs.androidx.compose.bom)) implementation(libs.androidx.ui) implementation(libs.androidx.ui.graphics) implementation(libs.androidx.material3) implementation(libs.androidx.activity.compose) debugImplementation(libs.androidx.ui.tooling) implementation("com.parse.bolts:bolts-tasks:1.4.0") implementation("org.apache.httpcomponents:httpclient-android:4.3.5.1") implementation("com.google.code.gson:gson:2.7") implementation(project(":mpcore")) //implementation("com.android.support:support-v4:28.0.0") }