peat-btle 0.2.4

Bluetooth Low Energy mesh transport for Peat Protocol
Documentation
// Copyright (c) 2025-2026 (r)evolve - Revolve Team LLC
// SPDX-License-Identifier: Apache-2.0
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

plugins {
    id("com.android.library")
    id("org.jetbrains.kotlin.android")
    id("maven-publish")
    id("signing")
}

group = "com.defenseunicorns"
version = "0.1.1"

android {
    namespace = "com.defenseunicorns.peat"
    compileSdk = 34

    defaultConfig {
        minSdk = 26  // Wear OS 3 minimum
        targetSdk = 34

        testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
        consumerProguardFiles("consumer-rules.pro")

        // Configure NDK for native library
        ndk {
            abiFilters += listOf("arm64-v8a", "armeabi-v7a", "x86_64")
        }

        // Build-time configuration for mesh credentials
        // Set via environment variables when building:
        //   PEAT_ENCRYPTION_SECRET=<64-char-hex> ./gradlew assembleRelease
        //   PEAT_MESH_ID=ALPHA ./gradlew assembleRelease
        // Downstream projects can override in their build.gradle.kts:
        //   buildConfigField("String", "PEAT_ENCRYPTION_SECRET", "\"...\"")
        buildConfigField("String", "PEAT_ENCRYPTION_SECRET",
            "\"${System.getenv("PEAT_ENCRYPTION_SECRET") ?: ""}\"")
        buildConfigField("String", "PEAT_MESH_ID",
            "\"${System.getenv("PEAT_MESH_ID") ?: ""}\"")
    }

    buildFeatures {
        buildConfig = true
    }

    buildTypes {
        release {
            isMinifyEnabled = false
            proguardFiles(
                getDefaultProguardFile("proguard-android-optimize.txt"),
                "proguard-rules.pro"
            )
        }
    }

    compileOptions {
        sourceCompatibility = JavaVersion.VERSION_17
        targetCompatibility = JavaVersion.VERSION_17
    }

    kotlinOptions {
        jvmTarget = "17"
    }

    // Source sets to include native libraries
    sourceSets {
        getByName("main") {
            jniLibs.srcDirs("src/main/jniLibs")
        }
    }

    // Configure publishing variant
    publishing {
        singleVariant("release") {
            withSourcesJar()
        }
    }
}

dependencies {
    implementation("androidx.core:core-ktx:1.12.0")
    implementation("androidx.annotation:annotation:1.7.1")
    implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3")

    // UniFFI uses JNA for FFI
    implementation("net.java.dev.jna:jna:5.14.0@aar")

    // Testing
    testImplementation("junit:junit:4.13.2")
    androidTestImplementation("androidx.test.ext:junit:1.1.5")
    androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
}

// Task to build native libraries using Cargo
tasks.register<Exec>("buildNativeLibs") {
    description = "Build native Rust libraries for Android"
    group = "build"

    // peat-btle root is parent of android directory
    val peatBtleRoot = rootProject.projectDir.parentFile
    workingDir = peatBtleRoot

    val ndkPath = System.getenv("ANDROID_NDK_HOME")
        ?: System.getenv("NDK_HOME")
        ?: "${System.getenv("ANDROID_HOME")}/ndk/27.0.12077973"

    environment("ANDROID_NDK_HOME", ndkPath)
    environment("PATH", "$ndkPath/toolchains/llvm/prebuilt/linux-x86_64/bin:${System.getenv("PATH")}")

    commandLine("bash", "-c", """
        set -e
        echo "Building peat-btle native libraries from: $(pwd)"

        # Build for arm64-v8a (modern Android devices)
        echo "Building for aarch64-linux-android (arm64-v8a)..."
        cargo build --release --target aarch64-linux-android --features android
        mkdir -p android/src/main/jniLibs/arm64-v8a
        cp target/aarch64-linux-android/release/libpeat_btle.so android/src/main/jniLibs/arm64-v8a/

        # Build for armeabi-v7a (older devices)
        echo "Building for armv7-linux-androideabi (armeabi-v7a)..."
        cargo build --release --target armv7-linux-androideabi --features android
        mkdir -p android/src/main/jniLibs/armeabi-v7a
        cp target/armv7-linux-androideabi/release/libpeat_btle.so android/src/main/jniLibs/armeabi-v7a/

        # Build for x86_64 (emulators)
        echo "Building for x86_64-linux-android (x86_64)..."
        cargo build --release --target x86_64-linux-android --features android
        mkdir -p android/src/main/jniLibs/x86_64
        cp target/x86_64-linux-android/release/libpeat_btle.so android/src/main/jniLibs/x86_64/

        echo ""
        echo "Native libraries built successfully!"
        echo "  arm64-v8a: android/src/main/jniLibs/arm64-v8a/libpeat_btle.so"
        echo "  armeabi-v7a: android/src/main/jniLibs/armeabi-v7a/libpeat_btle.so"
        echo "  x86_64: android/src/main/jniLibs/x86_64/libpeat_btle.so"
    """.trimIndent())
}

// Task to clean native libraries
tasks.register<Delete>("cleanNativeLibs") {
    description = "Clean native Rust libraries"
    group = "build"

    delete(
        "src/main/jniLibs/arm64-v8a/libpeat_btle.so",
        "src/main/jniLibs/armeabi-v7a/libpeat_btle.so",
        "src/main/jniLibs/x86_64/libpeat_btle.so"
    )
}

// Combined task: build native libs + assemble AAR
tasks.register("buildAar") {
    description = "Build native libraries and assemble AAR"
    group = "build"

    dependsOn("buildNativeLibs")
    finalizedBy("assembleRelease")
}

// Task to publish to local Maven for testing
tasks.register("publishLocal") {
    description = "Build and publish AAR to local Maven repository (~/.m2)"
    group = "publishing"

    dependsOn("buildNativeLibs")
    finalizedBy("publishToMavenLocal")
}

// Publishing configuration
afterEvaluate {
    publishing {
        publications {
            register<MavenPublication>("release") {
                groupId = "com.defenseunicorns"
                artifactId = "peat-btle"
                version = project.version.toString()

                from(components["release"])

                pom {
                    name.set("Peat BLE Android")
                    description.set("Bluetooth Low Energy mesh transport for Peat Protocol - Android library")
                    url.set("https://github.com/defenseunicorns/peat-btle")

                    licenses {
                        license {
                            name.set("Apache License 2.0")
                            url.set("https://www.apache.org/licenses/LICENSE-2.0")
                        }
                    }

                    developers {
                        developer {
                            id.set("defenseunicorns")
                            name.set("Defense Unicorns")
                            email.set("oss@defenseunicorns.com")
                        }
                    }

                    scm {
                        connection.set("scm:git:git://github.com/defenseunicorns/peat-btle.git")
                        developerConnection.set("scm:git:ssh://github.com/defenseunicorns/peat-btle.git")
                        url.set("https://github.com/defenseunicorns/peat-btle")
                    }
                }
            }
        }

        repositories {
            maven {
                name = "GitHubPackages"
                url = uri("https://maven.pkg.github.com/defenseunicorns/peat-btle")
                credentials {
                    username = project.findProperty("gpr.user") as String? ?: System.getenv("GITHUB_ACTOR")
                    password = project.findProperty("gpr.key") as String? ?: System.getenv("GITHUB_TOKEN")
                }
            }

            // Local staging repository for Central Portal bundle
            maven {
                name = "local"
                url = uri(layout.buildDirectory.dir("repo"))
            }
        }
    }

    // Sign all publications
    signing {
        val signingKey = findProperty("signingInMemoryKey") as String? ?: System.getenv("ORG_GRADLE_PROJECT_signingInMemoryKey")
        val signingPassword = findProperty("signingInMemoryKeyPassword") as String? ?: System.getenv("ORG_GRADLE_PROJECT_signingInMemoryKeyPassword")
        if (signingKey != null && signingPassword != null) {
            useInMemoryPgpKeys(signingKey, signingPassword)
        } else {
            useGpgCmd()
        }
        sign(publishing.publications["release"])
    }
}

// Task to create Maven Central bundle ZIP
tasks.register<Zip>("createMavenCentralBundle") {
    description = "Create ZIP bundle for Maven Central upload"
    group = "publishing"

    dependsOn("publishReleasePublicationToLocalRepository")

    from(layout.buildDirectory.dir("repo"))
    archiveFileName.set("peat-btle-${project.version}-bundle.zip")
    destinationDirectory.set(layout.buildDirectory.dir("bundle"))
}

// Task to publish to Maven Central via Central Portal API
tasks.register<Exec>("publishToMavenCentral") {
    description = "Upload bundle to Maven Central via Sonatype Central Portal"
    group = "publishing"

    dependsOn("createMavenCentralBundle")

    val bundleFile = layout.buildDirectory.file("bundle/peat-btle-${project.version}-bundle.zip")
    val username = project.findProperty("sonatypeUsername") as String? ?: System.getenv("SONATYPE_USERNAME") ?: ""
    val password = project.findProperty("sonatypePassword") as String? ?: System.getenv("SONATYPE_PASSWORD") ?: ""

    doFirst {
        if (username.isEmpty() || password.isEmpty()) {
            throw GradleException("Sonatype credentials not configured. Set sonatypeUsername and sonatypePassword in gradle.properties")
        }
    }

    commandLine("bash", "-c", """
        curl --fail-with-body \
            -u "$username:$password" \
            -F "bundle=@${bundleFile.get().asFile.absolutePath}" \
            "https://central.sonatype.com/api/v1/publisher/upload?publishingType=AUTOMATIC"
    """.trimIndent())
}