Android AAR package for native library

= UPDATE 2020-06-20 =

Nowadays, there is a nice plugin for that, works pretty well. Thanks to its author and to @Paulo Costa for pointing to it.

= OBSOLETE =

Found the following hacky solution to the problem:

Use Android Experimental Gradle plugin version 0.9.1. The idea is to put library headers and static libraries into .aar. The headers are put to ndkLibs/include and static libs to ndkLibs/<arch> for each architecture. Then, in the app, or another lib which depends on this packed lib we just extract ndkLibs directory from AAR to the build directory in the project. See the example gradle file below.

The build.gradle file for the library with comments:

apply plugin: "com.android.model.library"

model {
    android {
        compileSdkVersion = 25
        buildToolsVersion = '25.0.2'

        defaultConfig {
            minSdkVersion.apiLevel = 9
            targetSdkVersion.apiLevel = 9
            versionCode = 1
            versionName = '1.0'
        }
        ndk {
            platformVersion = 21
            moduleName = "mylib"
            toolchain = 'clang'
            abiFilters.addAll(['armeabi', 'armeabi-v7a', 'arm64-v8a', 'x86', 'x86_64', 'mips', 'mips64']) //this is default
            ldLibs.addAll(['android', 'log'])
            stl = 'c++_static'
            cppFlags.add("-std=c++11")
            cppFlags.add("-fexceptions")
            cppFlags.add("-frtti")

            //Add include path to be able to find headers from other AAR libraries
            cppFlags.add("-I" + projectDir.getAbsolutePath() + "/build/ndkLibs/include")
        }

        //For each ABI add link-time library search path to be able to link against other AAR libraries
        abis {
            create("armeabi") {
                ldFlags.add("-L" + projectDir.getAbsolutePath() + "/build/ndkLibs/armeabi")
            }
            create("armeabi-v7a") {
                ldFlags.add("-L" + projectDir.getAbsolutePath() + "/build/ndkLibs/armeabi-v7a")
            }
            create("arm64-v8a") {
                ldFlags.add("-L" + projectDir.getAbsolutePath() + "/build/ndkLibs/arm64-v8a")
            }
            create("x86") {
                ldFlags.add("-L" + projectDir.getAbsolutePath() + "/build/ndkLibs/x86")
            }
            create("x86_64") {
                ldFlags.add("-L" + projectDir.getAbsolutePath() + "/build/ndkLibs/x86_64")
            }
            create("mips") {
                ldFlags.add("-L" + projectDir.getAbsolutePath() + "/build/ndkLibs/mips")
            }
            create("mips64") {
                ldFlags.add("-L" + projectDir.getAbsolutePath() + "/build/ndkLibs/mips64")
            }
        }
    }

    //Configure this library source files
    android.sources {
        main {
            jni {
                //This does not affect AAR packaging
                exportedHeaders {
                    srcDir "../../src/"
                }

                //This tells which source files to compile
                source {
                    srcDirs '../../src'
                }
            }
        }
    }
}

//Custom Maven repository URLs to download AAR files from
repositories {
    maven {
        url 'https://dl.bintray.com/igagis/android/'
    }
}

//Our custom AAR dependencies, those in turn are also packed to AAR using the same approach
dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    compile 'io.github.igagis:libutki:+'
    compile 'io.github.igagis:libsvgdom:+'
    compile 'org.cairographics:cairo:+'
}


//===================================
//=== Extract NDK files from AARs ===
//This is to automatically extract ndkLibs directory from AAR to build directory before compiling any sources
task extractNDKLibs {
    doLast {
        configurations.compile.each {
            def file = it.absoluteFile
            copy {
                from zipTree(file)
                into "build/"
                include "ndkLibs/**/*"
            }
        }
    }
}
build.dependsOn('extractNDKLibs')
tasks.whenTaskAdded { task ->
    if (task.name.startsWith('compile')) {
        task.dependsOn('extractNDKLibs')
    }
}



//=================================
//=== pack library files to aar ===
//This stuff re-packs the release AAR file adding headers and static libs to there, but removing all shared (.so) libs, as we don't need them. The resulting AAR is put to the project root directory and can be uploaded to Maven along with POM file (you need to write one by hand).

def aarName = name

task copyNdkLibsToAAR(type: Zip) {
    baseName = aarName
    version = "\$(version)"
    extension = 'aar.in'
    destinationDir = file('..') //put resulting AAR file to upper level directory

    from zipTree("build/outputs/aar/" + aarName + "-release.aar")
    exclude('**/*.so') //do not include shared libraries into final AAR
    from("../../src") {
        exclude('makefile')
        exclude('soname.txt')
        exclude('**/*.cpp')
        exclude('**/*.c')
        into('ndkLibs/include')
    }
    from("build/intermediates/binaries/debug/lib"){
        include('**/*.a')
        into('ndkLibs')
    }
}

build.finalizedBy('copyNdkLibsToAAR')

Manually hacking the gradle scripts works, but is painful and error-prone.

I've recently found a plugin that magically bundles the headers into the AAR files and extracts them and sets up the build scripts when adding the dependency: https://github.com/howardpang/androidNativeBundle

On the reusable library:

  • Add the export plugin:

    apply plugin: 'com.ydq.android.gradle.native-aar.export'
    
  • Define where the header files are:

    nativeBundleExport {
        headerDir = "${project.projectDir}/src/main/jni/include"
    }
    

On the module that uses it:

  • Add the import plugin:

    apply plugin: 'com.ydq.android.gradle.native-aar.import'
    
  • add include ${ANDROID_GRADLE_NATIVE_BUNDLE_PLUGIN_MK} to each module that depends on it in your Android.mk:

    include $(CLEAR_VARS)
    LOCAL_SRC_FILES := myapp.cpp \
    LOCAL_MODULE := myapp
    LOCAL_LDLIBS += -llog
    include ${ANDROID_GRADLE_NATIVE_BUNDLE_PLUGIN_MK}
    include $(BUILD_SHARED_LIBRARY)