MeshChatX/android/app/build.gradle

404 lines
15 KiB
Groovy

plugins {
id 'com.android.application'
id 'com.chaquo.python'
}
def repoRoot = rootProject.projectDir.parentFile
def meshchatSourceDir = new File(repoRoot, "meshchatx")
def lxmfySourceDir = new File(repoRoot, "vendor/lxmfy/lxmfy")
def rnsFilesyncSourceDir = new File(repoRoot, "vendor/rns_filesync/rns_filesync")
def repositoryBundledWheelsDir = new File(meshchatSourceDir, "public/repository-server-bundled/bundled")
def chaquopyBuildPython = System.getenv("CHAQUOPY_BUILD_PYTHON")
def pythonTempDir = new File(buildDir, "python-tmp")
def vendorWheelDir = new File(rootProject.projectDir, "vendor")
def lxstPatchedWheel = new File(rootProject.projectDir, "vendor/lxst-0.5.1-py3-none-any.whl")
def bleakPatchedWheel = new File(rootProject.projectDir, "vendor/bleak-3.0.2-py3-none-any.whl")
def rnsPatchedWheel = new File(rootProject.projectDir, "vendor/rns-1.4.2-py3-none-any.whl")
def allAndroidAbis = ["arm64-v8a", "x86_64", "armeabi-v7a"]
def selectedAndroidAbis = (
project.findProperty("meshchatxAbis")
?: System.getenv("MESHCHATX_ABIS")
?: allAndroidAbis.join(",")
).split(",").collect { it.trim() }.findAll { it }
if (selectedAndroidAbis.isEmpty()) {
throw new org.gradle.api.GradleException("No Android ABIs selected. Use -PmeshchatxAbis or MESHCHATX_ABIS.")
}
def invalidAndroidAbis = selectedAndroidAbis.findAll { !allAndroidAbis.contains(it) }
if (!invalidAndroidAbis.isEmpty()) {
throw new org.gradle.api.GradleException("Unsupported ABIs: ${invalidAndroidAbis}. Supported: ${allAndroidAbis}")
}
def meshchatxAbiPackaging = (
project.findProperty("meshchatxAbiPackaging")
?: System.getenv("MESHCHATX_ABI_PACKAGING")
?: "universal"
).toString().trim().toLowerCase()
if (meshchatxAbiPackaging != "universal" && meshchatxAbiPackaging != "split") {
throw new org.gradle.api.GradleException(
"meshchatxAbiPackaging must be 'universal' or 'split' (got '${meshchatxAbiPackaging}'). " +
"Use -PmeshchatxAbiPackaging=... or MESHCHATX_ABI_PACKAGING."
)
}
def enableAbiSplits = meshchatxAbiPackaging == "split" && selectedAndroidAbis.size() > 1
android {
namespace 'com.meshchatx'
compileSdk 35
defaultConfig {
applicationId "com.meshchatx"
minSdk 24
targetSdk 35
versionCode 48002
versionName "4.8.2"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
ndk {
abiFilters(*selectedAndroidAbis as String[])
}
}
splits {
abi {
enable enableAbiSplits
reset()
if (enableAbiSplits) {
include(*selectedAndroidAbis as String[])
}
universalApk enableAbiSplits
}
}
buildTypes {
release {
minifyEnabled true
shrinkResources true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
packagingOptions {
jniLibs {
useLegacyPackaging = true
}
}
lint {
abortOnError true
checkReleaseBuilds false
}
}
tasks.register("syncCodec2JniLibs", Exec) {
workingDir = repoRoot
def abiArg = selectedAndroidAbis.join(",")
commandLine(
"bash",
"scripts/android/sync-codec2-jni-libs.sh",
vendorWheelDir.absolutePath,
"${projectDir}/src/main/jniLibs",
abiArg
)
doFirst {
if (!vendorWheelDir.isDirectory()) {
throw new org.gradle.api.GradleException(
"Missing android/vendor directory at ${vendorWheelDir}. " +
"Run: bash scripts/build-android-wheels-local.sh"
)
}
def wheels = vendorWheelDir.list()?.toList() ?: []
if (!wheels.any { it.startsWith("chaquopy_libcodec2-") }) {
throw new org.gradle.api.GradleException(
"Missing chaquopy_libcodec2 wheels in ${vendorWheelDir}. " +
"Run: bash scripts/build-android-wheels-local.sh"
)
}
}
}
tasks.register("verifyVendorWheels") {
doLast {
def vendorPackages = [
"aiohttp": "3.14.3",
"cbor2": "5.6.5",
"cryptography": "50.0.0",
]
def purePythonVendorPrefixes = [
"httpx-0.28.1-",
"httpcore-",
"h2-",
"h11-",
"anyio-",
"hpack-",
"hyperframe-",
]
def wheels = vendorWheelDir.list()?.toList() ?: []
purePythonVendorPrefixes.each { prefix ->
if (!wheels.any { it.startsWith(prefix) && it.endsWith(".whl") }) {
throw new org.gradle.api.GradleException(
"Missing ${prefix}*.whl in ${vendorWheelDir} (httpx/RNS-over-HTTP). " +
"Run: bash scripts/build-android-wheels-local.sh"
)
}
}
selectedAndroidAbis.each { abi ->
def abiTag = abi.replace("-", "_")
vendorPackages.each { pkg, ver ->
def prefix = "${pkg}-${ver}-"
if (!wheels.any { it.startsWith(prefix) && it.contains("android_24_${abiTag}") }) {
throw new org.gradle.api.GradleException(
"Missing ${pkg} ${ver} wheel for ${abi} in ${vendorWheelDir}. " +
"Run: bash scripts/build-android-wheels-local.sh --only-recipes ${pkg}-*"
)
}
}
if (!wheels.any { it.startsWith("pycodec2-") && it.contains("android_24_${abiTag}") }) {
throw new org.gradle.api.GradleException(
"Missing pycodec2 wheel for ${abi} in ${vendorWheelDir}. " +
"Run: bash scripts/build-android-wheels-local.sh"
)
}
if (!wheels.any { it.startsWith("chaquopy_libcodec2-") && it.contains("android_24_${abiTag}") }) {
throw new org.gradle.api.GradleException(
"Missing chaquopy_libcodec2 wheel for ${abi} in ${vendorWheelDir}. " +
"Run: bash scripts/build-android-wheels-local.sh"
)
}
def jniLib = file("${projectDir}/src/main/jniLibs/${abi}/libcodec2.so")
if (!jniLib.isFile() || jniLib.length() < 100_000) {
throw new org.gradle.api.GradleException(
"Missing or tiny jniLibs/${abi}/libcodec2.so after sync. " +
"Expected a real Codec2 shared library for Android calls."
)
}
}
}
}
tasks.configureEach { task ->
if (task.name == "preBuild") {
task.dependsOn(tasks.named("syncCodec2JniLibs"), tasks.named("verifyVendorWheels"))
}
}
tasks.register("repackAndroidPycodec2Wheels", Exec) {
workingDir = repoRoot
def pyExe = (System.getenv("MESHCHATX_REPACK_PYTHON") ?: "python3")
commandLine(pyExe, "scripts/repack-android-pycodec2-wheels.py", "--vendor-dir", vendorWheelDir.absolutePath)
doFirst {
if (!vendorWheelDir.isDirectory()) {
throw new org.gradle.api.GradleException(
"Missing android/vendor directory at ${vendorWheelDir}"
)
}
def wheels = vendorWheelDir.list()?.toList() ?: []
if (!wheels.any { it.startsWith("pycodec2-") && it.endsWith(".whl") }) {
throw new org.gradle.api.GradleException(
"Missing pycodec2 wheels in ${vendorWheelDir}. " +
"Run: bash scripts/build-android-wheels-local.sh"
)
}
if (!wheels.any { it.startsWith("chaquopy_libcodec2-") && it.endsWith(".whl") }) {
throw new org.gradle.api.GradleException(
"Missing chaquopy_libcodec2 wheels needed to repack pycodec2 in ${vendorWheelDir}"
)
}
}
}
tasks.named("syncCodec2JniLibs").configure {
dependsOn(tasks.named("repackAndroidPycodec2Wheels"))
}
tasks.named("verifyVendorWheels").configure {
dependsOn(tasks.named("syncCodec2JniLibs"))
}
tasks.register("fetchRepositoryBundledWheels", Exec) {
workingDir = repoRoot
def pyExe = (System.getenv("MESHCHATX_FETCH_PYTHON") ?: "python3")
commandLine(pyExe, "scripts/build/fetch_repository_wheels.py")
ignoreExitValue = false
onlyIf {
if (System.getenv("MESHCHATX_OFFLINE_BUILD") == "1") {
if (!repositoryBundledWheelsDir.isDirectory() || !repositoryBundledWheelsDir.list()?.any { it.endsWith(".whl") }) {
throw new org.gradle.api.GradleException("MESHCHATX_OFFLINE_BUILD=1 but repository bundled wheels are missing at ${repositoryBundledWheelsDir}")
}
return false
}
if (System.getenv("MESHCHATX_SKIP_REPOSITORY_WHEELS_FETCH") == "1") {
return false
}
if (!repositoryBundledWheelsDir.isDirectory()) {
return true
}
def names = repositoryBundledWheelsDir.list()
return names == null || !names.any { it.endsWith(".whl") }
}
}
tasks.register("syncMeshchatPython", Sync) {
dependsOn("fetchRepositoryBundledWheels")
if (!meshchatSourceDir.exists()) {
throw new org.gradle.api.GradleException("Missing meshchatx source directory at ${meshchatSourceDir}")
}
doFirst {
delete(
file("$projectDir/src/main/python/meshchatx"),
file("$projectDir/src/main/python/LXST"),
file("$projectDir/src/main/python/LXMF"),
file("$projectDir/src/main/python/RNS")
)
}
from(meshchatSourceDir)
includeEmptyDirs = false
from(new File(repoRoot, "CHANGELOG.md"))
into(file("$projectDir/src/main/python/meshchatx"))
}
/*
* Desktop installs lxmfy via setuptools (pyproject where=[".", "vendor/lxmfy"]).
* Chaquopy only sees android/app/src/main/python plus pip installs, so the
* vendored package must be synced here or bot_templates import fails at boot.
*/
tasks.register("syncLxmfyPython", Sync) {
if (!lxmfySourceDir.exists()) {
throw new org.gradle.api.GradleException(
"Missing vendored lxmfy package at ${lxmfySourceDir}. " +
"Expected vendor/lxmfy/lxmfy from the MeshChatX tree."
)
}
def initPy = new File(lxmfySourceDir, "__init__.py")
if (!initPy.exists()) {
throw new org.gradle.api.GradleException("Missing ${initPy}; vendored lxmfy tree looks incomplete.")
}
doFirst {
delete(file("$projectDir/src/main/python/lxmfy"))
}
from(lxmfySourceDir)
includeEmptyDirs = false
into(file("$projectDir/src/main/python/lxmfy"))
}
/*
* Desktop installs rns_filesync via setuptools (pyproject where includes vendor/rns_filesync).
* Chaquopy only sees android/app/src/main/python plus pip installs, so the
* vendored package must be synced here or FileSync imports fail at boot.
*/
tasks.register("syncRnsFilesyncPython", Sync) {
if (!rnsFilesyncSourceDir.exists()) {
throw new org.gradle.api.GradleException(
"Missing vendored rns_filesync package at ${rnsFilesyncSourceDir}. " +
"Expected vendor/rns_filesync/rns_filesync from the MeshChatX tree."
)
}
def initPy = new File(rnsFilesyncSourceDir, "__init__.py")
if (!initPy.exists()) {
throw new org.gradle.api.GradleException("Missing ${initPy}; vendored rns_filesync tree looks incomplete.")
}
doFirst {
delete(file("$projectDir/src/main/python/rns_filesync"))
}
from(rnsFilesyncSourceDir)
includeEmptyDirs = false
into(file("$projectDir/src/main/python/rns_filesync"))
}
tasks.configureEach { task ->
if (task.name.toLowerCase().contains("merge") && task.name.toLowerCase().contains("pythonsources")) {
task.dependsOn(
tasks.named("syncMeshchatPython"),
tasks.named("syncLxmfyPython"),
tasks.named("syncRnsFilesyncPython"),
)
}
}
tasks.configureEach { task ->
if (task.name.toLowerCase().contains("python")) {
task.doFirst {
if (!pythonTempDir.exists()) {
pythonTempDir.mkdirs()
}
if (task.metaClass.respondsTo(task, "environment", String, Object)) {
task.environment("TMPDIR", pythonTempDir.absolutePath)
task.environment("TEMP", pythonTempDir.absolutePath)
task.environment("TMP", pythonTempDir.absolutePath)
}
}
}
}
chaquopy {
defaultConfig {
version "3.11"
if (chaquopyBuildPython != null && !chaquopyBuildPython.trim().isEmpty()) {
buildPython chaquopyBuildPython
}
pip {
if (!vendorWheelDir.exists()) {
throw new org.gradle.api.GradleException("Missing vendor wheel directory at ${vendorWheelDir}")
}
options "--find-links", vendorWheelDir.absolutePath
install "packaging>=23"
install "aiohttp==3.14.3"
if (!rnsPatchedWheel.exists()) {
throw new org.gradle.api.GradleException("Missing patched RNS wheel at ${rnsPatchedWheel}")
}
// Patched so RNS's Android RNodeInterface never calls RNS.panic() (os._exit)
// when usbserial4a/jnius are missing. MeshChatX also ships a Chaquopy jnius
// shim, usb4a context bridge, and able BLE stack so USB/BT/BLE RNode can
// run. RNode over TCP needs neither native module and always works.
install rnsPatchedWheel.absolutePath
install "lxmf>=1.1.1"
install "numpy==1.26.2"
install "chaquopy-libcodec2==1.2.0"
install "pycodec2==4.1.1"
install "miniaudio==1.70"
if (!lxstPatchedWheel.exists()) {
throw new org.gradle.api.GradleException("Missing patched LXST wheel at ${lxstPatchedWheel}")
}
install lxstPatchedWheel.absolutePath
if (!bleakPatchedWheel.exists()) {
throw new org.gradle.api.GradleException("Missing patched bleak wheel at ${bleakPatchedWheel}")
}
install bleakPatchedWheel.absolutePath
install "psutil>=7.2.2"
install "websockets>=16.0"
install "bcrypt==3.1.7"
install "aiohttp-session>=2.12.1,<3.0.0"
install "cryptography==50.0.0"
install "pycparser>=3.0"
install "pyserial>=3.5"
install "usbserial4a==0.4.0"
install "ply>=3.11,<4.0"
install "cbor2==5.6.5"
// Pure-python; wheels are vendored by scripts/build-android-wheels-local.sh
// for offline/find-links installs (HTTPInterface / RNS-over-HTTP).
install "httpx[http2]==0.28.1"
install "altcha==2.0.2"
}
}
}
dependencies {
implementation 'androidx.core:core:1.16.0'
implementation 'com.squareup.okhttp3:okhttp:4.12.0'
implementation 'androidx.appcompat:appcompat:1.7.1'
implementation 'com.google.android.material:material:1.13.0'
implementation 'androidx.constraintlayout:constraintlayout:2.2.1'
implementation 'androidx.webkit:webkit:1.14.0'
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.8.7'
implementation 'com.github.mik3y:usb-serial-for-android:3.8.1'
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.2.1'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.6.1'
}