Gradle android build for different processor architectures
Asked Answered
L

4

33

I want to build 4 separate apks for 4 different Android CPU processor architectures (armeabi armeabi-v7a x86 mips) using Gradle.

I have native OpenCV libraries built for 4 CPU architectures in the libs folder.

libs
    -armeabi
    -armeabi-v7a
    -x86
    -mips

I want to each apk only contains the OpenCV library corresponding to the correct CPU architecture.

The current build script is as below:

apply plugin: 'android'

dependencies {
    compile fileTree(dir: 'libs', include: '*.jar')
    compile project(':workspace:OpenCV4Android:sdk:java')
}

android {
    compileSdkVersion 11
    buildToolsVersion "18.1.0"

    sourceSets {
        main {
            manifest.srcFile 'AndroidManifest.xml'
            java.srcDirs = ['src']
            resources.srcDirs = ['src']
            aidl.srcDirs = ['src']
            renderscript.srcDirs = ['src']
            res.srcDirs = ['res']
            assets.srcDirs = ['assets']
        }

        // Move the tests to tests/java, tests/res, etc...
        instrumentTest.setRoot('tests')

        debug.setRoot('build-types/debug')
        release.setRoot('build-types/release')

        flavorGroups "abi", "version"
        productFlavors {
            x86 {
                flavorGroup "abi"
            }
            arm {
                flavorGroup "abi"
            }
            mips {
                flavorGroup "abi"
            }
        }

    }
}

Can someone help me to resolve this please?

Cheers,

Lebkuchen answered 9/10, 2013 at 9:51 Comment(1)
did you solve your problem? I need such solution too, although I'm still using ANT build. Would switch to gradle if the above had a clear answer. Tried to approach the problem from the opposite end: delete the unneeded native libs from .apk (which is really a zip file) and re-signing it. That's easy, but we also need to modify AndroidManifest.xml to put slightly different versionCode for each platform (also: lower for armeabi and higher for armeabi-v7a), and this file is in compiled binary form, difficult to modify...Salamanca
T
25

As of Android Gradle Plugin version 13 you can now generate seperate APK's using the new "split" mechanism. You can read about it here.

The default file structure for placing your .so files is:

src
-main
  -jniLibs
    -armeabi
      -arm.so
    -armeabi-v7a
      -armv7.so
    -x86
      -x86.so
    -mips
      -mips.so

Note that the name of the .so file is unimportant as long as it has the .so extension.

Then in your Gradle build file:

android {
...
splits {
abi {
  enable true
  reset()
  include 'x86', 'armeabi-v7a', 'mips', 'armeabi'
  universalApk false
  }
 }
}

and

// map for the version code
ext.versionCodes = ['armeabi-v7a':1, mips:2, x86:3]

import com.android.build.OutputFile

android.applicationVariants.all { variant ->
    // assign different version code for each output
    variant.outputs.each { output ->
        output.versionCodeOverride =
            project.ext.versionCodes.get(output.getFilter(OutputFile.ABI)) * 1000000 + android.defaultConfig.versionCode
    }
}

Note that the version codes above in ext.versionCodes are largely irrelevant, this is here to add a unique offset for each ABI type so version codes do not clash.

Tuyettv answered 30/9, 2014 at 20:25 Comment(12)
did it similar to this before - but since updating to build-tool 1.0.0 I run into this issue with it: Could not find property 'abiFilter' on com.android.build.gradle.internal.api.ApkVariantOutputImpl_Decorated@61a2abff.Meggy
@Meggy I've update the example above with the latest Google documentation.Tuyettv
To which build.gradle file do these modifications go? The project one or the main module "app" one? And also where do we add that second batch of code?? Under which block? Could you please show us a full build.gradle sample codeBandung
@PaulAsiimwe which gradle file do you have your 'android' block in...Then the second block goes outside of the android block.Tuyettv
Thanks for the details. I was getting an error 'cannot perform mulitply() on null' because output.getFilter(OutputFile.ABI) was returning 'armabi' and there is no entry for that in the versionCodes map. I added it with a value of 1 and incremented all the others and it now works. Have you tried the density split at all? Does that require further versioncode trickery to successfully split apk's by abi and density?Evanish
If anyone is interested, the answer to my question about density splitting lies here with a link to example code: #20600340Evanish
do we require to change the versionCode for each binary?Mcferren
@Mcferren yes, if you see the bottom of my example, this produces version codes for each binary that shouldn't clash whenever you update your applications.Tuyettv
thanks. If I make the universalAPK to true, I am getting an error in gradle : cannot invoke multiply() on null object . Is there a way to specify the universalAPK in the ext.versionCodes map ?Mcferren
@Tuyettv : What I meant was that I have the splits along with a UniversalAPK (to distribute it privately to QA).Mcferren
@Mcferren I was messing around with this today and found that, indeed, setting universalApk to true will actually produce all the split apk's AND a universalApk, so I was incorrect.Tuyettv
@Tuyettv but this works if we don't change the versionCode using the array approach .Mcferren
L
15

The split ABI APK solution for gradle is the simplest I have found so far. @withoutclass has a good writeup here: https://mcmap.net/q/412339/-gradle-android-build-for-different-processor-architectures I had to reference the Android documentation since this is a new feature that can still change: http://tools.android.com/tech-docs/new-build-system/user-guide/apk-splits

However, I ended up having to abandon this simple implementation since I needed to support both a fat build and architecture specific builds. You might encounter this same issue if you support both the Google Play store (which supports architecture specific APKs) and the Amazon Appstore (which supports only fat APKs).

It might be possible to do this with split APKs if you can add a flavor component, but as of now split+flavor is not yet supported: https://code.google.com/p/android/issues/detail?id=76469

I ended up using the abiFilter, see sample code below:

android {
    flavorDimensions "abi"

    productFlavors {
        fat {
            flavorDimension "abi"
            ndk {
                abiFilters "x86", "armeabi-v7a", "armeabi"
                versionCode = 0;
            }
        }
        arm {
            flavorDimension "abi"
            ndk {
                abiFilter "armeabi"
                versionCode = 1;
            }
        }
        armv7a {
            flavorDimension "abi"
            ndk {
                abiFilter "armeabi-v7a"
                versionCode = 3;
            }
        }
        x86 {
            flavorDimension "abi"
            ndk {
                abiFilter "x86"
                versionCode = 6;
            }
        }
    }
}

// Each APK needs a different version code when submitted to Google,
// bump the versionCode we set in defaultConfig
android.applicationVariants.all { variant ->
    // Ugly hard coded flavorDimensions position
    // If you have more than one flavorDimension, make sure to target the position for "abi"
    def abiVersion = variant.productFlavors.get(0).versionCode

    variant.mergedFlavor.versionCode = abiVersion * 1000 + android.defaultConfig.versionCode
}

Update Using universalApk set to true solves this solution, simply adds time to build each apk.

android {
    // Rest of Gradle file
        splits {
            abi {
            enable true
            reset()
            include 'armeabi', 'armeabi-v7a', 'x86'
            universalApk true
        }
    }
}

//Ensures architecture specific APKs have a higher version code
//(otherwise an x86 build would end up using the arm build, which x86 devices can run)
ext.versionCodes = [armeabi:1, 'armeabi-v7a':3, x86:6]

android.applicationVariants.all { variant ->
    // assign different version code for each output
    variant.outputs.each { output ->
        int abiVersionCode = project.ext.versionCodes.get(output.getFilter(OutputFile.ABI)) ?: 0
        output.versionCodeOverride = (abiVersionCode * 1000) + android.defaultConfig.versionCode
    }
}
Lynch answered 3/12, 2014 at 22:46 Comment(3)
Can you not solve this by changing the universialApk flag for your amazon flavor?Tuyettv
@Tuyettv didn't think about that, but it works! My build time is extraordinarily long (takes about 20 seconds to make a new build after one line change). Using APK splits with a universal build adds about 30 seconds to this process, using --profile gradle flag to measure results. Are you seeing a similar increase in build time?Lynch
I personally don't have a use for this at the moment. It wouldn't surprise me much though that packaging all the variants into a single apk would take some extra time.Tuyettv
A
4

UPDATE - since the time of this posting, there have been much progress in the gradle build process, thus this answer might not be the recommended best practice and new changes might even brake it. Use your own discretion.

To do that, first, you must put the native libraries in the following folder hierarchy separately

lib
 -armeabi
  -arm.so
  -*.so

-

lib
 -x86
  -x86.so
  -*.so

then zip the lib(without 's') folders (e.g. arm.zip and x86.zip) and rename the 'zip' extension to 'jar' (e.g. arm.jar and x86.jar). Put these jars in a appropriate folders (e.g. armeabi/libs and x86/libs). Now we are going to include the dependencies for each flavor. But we cannot use "compile file '....'". We have to use "flavorCompile file '...'"

e.g.

    flavorGroups 'abi'
        productFlavors {
            arm {
                flavorGroup 'abi'
                dependencies {
                    armCompile files('arm/libs/armeabi.jar')
                }
            }
            x86 {
                flavorGroup 'abi'
                dependencies {
                    x86Compile files('x86/libs/x86.jar')
                }
            }

    }

====

Here is more complex environment. You not only have processor architecture variants but you also have debug libraries(.jar,.so) for the processors. The example here has as Debug.jar for Arm debug and NonDebug.jar for Arm release; and *.so for both Arm and X86. Such configuration can be achieved by using gradle ExtraPropertiesExtension Please read my SO answer here, https://mcmap.net/q/411126/-build-variants-in-gradle-for-a-library-project-in-android , to understand how the debug folders can be structured.

android {
compileSdkVersion 18
buildToolsVersion "19.0.0"

final DEBUG_ROOT = "build-types/debug"
final RELEASE_ROOT = "build-types/release"
project.ext.set("projRoot", "")
buildTypes {
    debug {
        project.projRoot = DEBUG_ROOT

        dependencies {
            debugCompile files(DEBUG_ROOT+"/libs/Debug.jar")
        }
    }

    release {
        project.projRoot = RELEASE_ROOT
        dependencies {
            releaseCompile files(RELEASE_ROOT+"/libs/NonDebug.jar")
        }
        runProguard true
        proguardFile 'proguard.cfg'
    }
}
sourceSets {

    final PROJ_ROOT = project.ext.get("projRoot")
    final BUILD_TYPE_RES = PROJ_ROOT + "/res"
    main {
        manifest.srcFile 'src/main/AndroidManifest.xml'
        java.srcDirs = ['src/main/java']
        //resources.srcDirs = ['src/main']
        //aidl.srcDirs = ['src/main']
        //renderscript.srcDirs = ['src/main']
        res.srcDirs = ['src/main/res',BUILD_TYPE_RES]
        assets.srcDirs = ['src/main/assets']
    }

    flavorGroups 'abi'
    productFlavors {
        arm {
            flavorGroup 'abi'
            final ARM_LIB_PATH = PROJ_ROOT + "/arm/libs/armeabi.jar"
            dependencies {
                armCompile files(ARM_LIB_PATH)
            }
        }
        x86 {
            flavorGroup 'abi'
            final X86_LIB_PATH = PROJ_ROOT + "/x86/libs/x86.jar"
            dependencies {
                x86Compile files(X86_LIB_PATH)
            }
        }

    }

    // Move the tests to tests/java, tests/res, etc...
    instrumentTest.setRoot('tests')

    // Move the build types to build-types/<type>
    // For instance, build-types/debug/java, build-types/debug/AndroidManifest.xml, ...
    // This moves them out of them default location under src/<type>/... which would
    // conflict with src/ being used by the main source set.
    // Adding new build types or product flavors should be accompanied
    // by a similar customization.
    debug.setRoot(DEBUG_ROOT)
    release.setRoot(RELEASE_ROOT)
}

}

Adjustment answered 12/11, 2013 at 0:39 Comment(0)
S
3

I don't have a gradle answer, but I think I now have a generic answer for any Android build tool. Here is my idea on how to create separate APK files for each supported processor architecture:

  1. Build your APK with any tools you use, containing all native code libraries you support, e.g. armeabi, armeabi-v7a, x86 and mips. I'll call it the 'original' APK file.

  2. Unzip your original APK into an empty folder, with any zip/unzip utility, best use command line tools, so that you could automate it with a shell script or batch file later.

  3. In the folder where original APK was uncompressed to, delete META-INF sub-folder (this contains the signatures, we'll need to re-sign the APK after all the modifications, so the original META-INF must be deleted).

  4. Change to lib sub-folder, and delete the sub-folders for any processor architectures you don't want in the new APK file. For example, leave only 'x86' sub-folder to make an APK for Intel Atom processors.

  5. Important: each APK for a different architecture, must have a different 'versionCode' number in AndroidManifest.xml, and the version code for e.g. armeabi-v7a must be slightly higher than the one for armeabi (read Google directions for creating multiple APKs here: http://developer.android.com/google/play/publishing/multiple-apks.html ). Unfortunately, the manifest file is in a compiled binary form inside the APK. We need a special tool for modifying the versionCode there. See below.

  6. Once the manifest is modified with a new version code, and unnecessary directories and files deleted, re-zip, sign and align your smaller APK (use jarsigner and zipalign tools from Android SDK).

  7. Repeat the process for all other architectures you need to support, creating smaller APK files with slightly different version codes (but the same version name).

The only outstanding issue is the way to modify ‘versionCode’ in binary manifest file. I could not find a solution for this for a long time, so finally had to sit down and crank my own code to do this. As the starting point, I took APKExtractor by Prasanta Paul, http://code.google.com/p/apk-extractor/, written in Java. I’m the old school and still more comfortable with C++, so my little utility program 'aminc' written in C++ is now on GitHub at:

https://github.com/gregko/aminc

I posted the entire Visual Studio 2012 solution, but the whole program is a single .cpp file which probably can be compiled on any platform. And here is a sample Windows .bat file I use to split my "fat" apk named atVoice.apk into 4 smaller files named atVoice_armeabi.apk, atVoice_armeabi-v7a.apk, atVoice_x86.apk and atVoice_mips.apk. I actually submit these files to Google Play (see my app at https://play.google.com/store/apps/details?id=com.hyperionics.avar) and all works perfectly. Please also see this Github project by Jorge Suárez de Lis, who posts a similar script for Linux.

@echo off
REM    My "fat" apk is named atVoice.apk. Change below to whatever or set from %1
set apkfile=atVoice
del *.apk

REM    My tools build atVoice-release.apk in bin project sub-dir. 
REM    Copy it here for splitting.
copy ..\bin\%apkfile%-release.apk %apkfile%.apk

zip -d %apkfile%.apk META-INF/*

REM ------------------- armeabi ------------------------
unzip %apkfile%.apk AndroidManifest.xml
copy/y %apkfile%.apk %apkfile%.zip
zip -d %apkfile%.zip lib/armeabi-v7a/* lib/x86/* lib/mips/*
aminc AndroidManifest.xml 1
zip -f %apkfile%.zip
ren %apkfile%.zip %apkfile%_armeabi.apk
jarsigner -verbose -sigalg SHA1withRSA -digestalg SHA1 -keystore d:\users\greg\.android\Hyperionics.keystore -storepass MyPass %apkfile%_armeabi.apk MyKeyName
zipalign 4 %apkfile%_armeabi.apk %apkfile%_armeabi-aligned.apk
del %apkfile%_armeabi.apk
ren %apkfile%_armeabi-aligned.apk %apkfile%_armeabi.apk

REM ------------------- armeabi-v7a ---------------------
copy/y %apkfile%.apk %apkfile%.zip
zip -d %apkfile%.zip lib/armeabi/* lib/x86/* lib/mips/*
aminc AndroidManifest.xml 1
zip -f %apkfile%.zip
ren %apkfile%.zip %apkfile%_armeabi-v7a.apk
jarsigner -verbose -sigalg SHA1withRSA -digestalg SHA1 -keystore d:\users\greg\.android\Hyperionics.keystore -storepass MyPass %apkfile%_armeabi-v7a.apk MyKeyName
zipalign 4 %apkfile%_armeabi-v7a.apk %apkfile%_armeabi-v7a-aligned.apk
del %apkfile%_armeabi-v7a.apk
ren %apkfile%_armeabi-v7a-aligned.apk %apkfile%_armeabi-v7a.apk

REM ------------------- x86 ---------------------
copy/y %apkfile%.apk %apkfile%.zip
zip -d %apkfile%.zip lib/armeabi/* lib/armeabi-v7a/* lib/mips/*
aminc AndroidManifest.xml 9
zip -f %apkfile%.zip
ren %apkfile%.zip %apkfile%_x86.apk
jarsigner -verbose -sigalg SHA1withRSA -digestalg SHA1 -keystore d:\users\greg\.android\Hyperionics.keystore -storepass MyPass %apkfile%_x86.apk MyKeyName
zipalign 4 %apkfile%_x86.apk %apkfile%_x86-aligned.apk
del %apkfile%_x86.apk
ren %apkfile%_x86-aligned.apk %apkfile%_x86.apk

REM ------------------- MIPS ---------------------
copy/y %apkfile%.apk %apkfile%.zip
zip -d %apkfile%.zip lib/armeabi/* lib/armeabi-v7a/* lib/x86/*
aminc AndroidManifest.xml 10
zip -f %apkfile%.zip
ren %apkfile%.zip %apkfile%_mips.apk
jarsigner -verbose -sigalg SHA1withRSA -digestalg SHA1 -keystore d:\users\greg\.android\Hyperionics.keystore -storepass MyPass %apkfile%_mips.apk MyKeyName
zipalign 4 %apkfile%_mips.apk %apkfile%_mips-aligned.apk
del %apkfile%_mips.apk
ren %apkfile%_mips-aligned.apk %apkfile%_mips.apk


del AndroidManifest.xml
del %apkfile%.apk
:done

Greg

Salamanca answered 23/10, 2013 at 23:44 Comment(4)
Hi Greg, thanks for your alternative solution. I haven't tried it though, as my project is not that urgent to solve this problem. I will wait and see if Google provide more document on this.Lebkuchen
I liked your solution and I have adapted it to work on Linux. Credits given, of course. github.com/gentakojima/apk-slice-by-archCatacaustic
@JorgeSuárezdeLis - thank you, Jorge! I also inserted a link to your solution into the text of my post now. Yes, I still use Windows even for Android work, because Windows products are still my daily bread winners. Hard to make a living from Android apps!Salamanca
@JorgeSuárezdeLis - I also modified the 'aminc' Github repo adding the missing <stdlib.h> include. Thanks again!Salamanca

© 2022 - 2024 — McMap. All rights reserved.