/**
* This is free and unencumbered software released into the public domain.
Anyone is free to copy, modify, publish, use, compile, sell, or
distribute this software, either in source code form or as a compiled
binary, for any purpose, commercial or non-commercial, and by any
means.
In jurisdictions that recognize copyright laws, the author or authors
of this software dedicate any and all copyright interest in the
software to the public domain. We make this dedication for the benefit
of the public at large and to the detriment of our heirs and
successors. We intend this dedication to be an overt act of
relinquishment in perpetuity of all present and future rights to this
software under copyright law.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.
For more information, please refer to
*/
import com.android.annotations.NonNull
import com.android.manifmerger.ManifestMerger2
import com.android.manifmerger.ManifestMerger2.Invoker
import com.android.manifmerger.ManifestMerger2.MergeType
import com.android.manifmerger.MergingReport
import com.android.manifmerger.PlaceholderEncoder
import com.android.manifmerger.XmlDocument
import com.android.utils.ILogger
import com.google.common.base.Charsets
import com.google.common.io.Files
/**
* Fat AAR Lib generator v 0.1
* Latest version available at https://github.com/adwiv/android-fat-aar
* Please report issues at https://github.com/adwiv/android-fat-aar/issues
*
* This code is in public domain.
*
* Use at your own risk and only if you understand what it does. You have been warned ! :-)
*/
buildscript {
repositories {
jcenter()
}
dependencies {
classpath 'com.android.tools.build:manifest-merger:24.3.1'
}
}
configurations {
embedded
}
dependencies {
compile configurations.embedded
}
ext.embeddedAarDirs = new ArrayList()
afterEvaluate {
// the list of dependency must be reversed to use the right overlay order.
def dependencies = new ArrayList(configurations.embedded.resolvedConfiguration.firstLevelModuleDependencies)
dependencies.reverseEach {
def aarPath = "build/intermediates/exploded-aar/${it.moduleGroup}/${it.moduleName}/${it.moduleVersion}"
if (!embeddedAarDirs.contains(aarPath)) {
embeddedAarDirs.add(aarPath)
}
}
// Merge Assets
generateReleaseAssets.dependsOn embedAssets
// Embed Resources by overwriting the inputResourceSets
packageReleaseResources.dependsOn embedLibraryResources
// Merge Native libraries
if (tasks.findByPath('packageReleaseJniLibs') != null) {
embedJniLibs.mustRunAfter packageReleaseJniLibs
} else if (tasks.findByPath('mergeReleaseJniLibFolders') != null) {
embedJniLibs.mustRunAfter mergeReleaseJniLibFolders
} else {
NEEDTOFIX
}
bundleRelease.dependsOn embedJniLibs
// Merge Embedded Manifests
embedManifests.mustRunAfter processReleaseManifest
bundleRelease.dependsOn embedManifests
// Generate R.java for each embedded package and ensure it is compiled
embedRClassFiles.mustRunAfter processReleaseResources
// Merge proguard files
embedLibraryResources.dependsOn embedProguard
if (tasks.findByPath('compileReleaseJava') != null) {
compileReleaseJava.dependsOn embedRClassFiles
embedJavaJars.mustRunAfter compileReleaseJava
} else if (tasks.findByPath('compileReleaseJavaWithJavac') != null) {
compileReleaseJavaWithJavac.dependsOn embedRClassFiles
embedJavaJars.mustRunAfter compileReleaseJavaWithJavac
} else {
NEEDTOFIX
}
if (tasks.findByPath('proguardRelease') != null) {
proguardRelease.dependsOn embedJavaJars
}
bundleRelease.dependsOn embedJavaJars
}
task embedLibraryResources << {
def oldInputResourceSet = packageReleaseResources.inputResourceSets
packageReleaseResources.conventionMapping.map("inputResourceSets") {
getMergedInputResourceSets(oldInputResourceSet)
}
}
private List getMergedInputResourceSets(List inputResourceSet) {
//We need to do this trickery here since the class declared here and that used by the runtime
//are different and results in class cast error
def ResourceSetClass = inputResourceSet.get(0).class
List newInputResourceSet = new ArrayList(inputResourceSet)
embeddedAarDirs.each { aarPath ->
try {
def resname = (aarPath.split("build/intermediates/exploded-aar/")[1]).replace('/', ':');
def rs = ResourceSetClass.newInstance([resname] as Object[])
rs.addSource(file("$aarPath/res"))
// println "ResourceSet is " + rs
newInputResourceSet += rs
} catch (Exception e) {
e.printStackTrace();
throw e;
}
}
return newInputResourceSet
}
/**
* Assets are simple files, so just adding them to source set seems to work.
*/
task embedAssets << {
embeddedAarDirs.each { aarPath ->
// Merge Assets
android.sourceSets.main.assets.srcDirs += file("$aarPath/assets")
}
}
/**
* Merge proguard.txt files from all library modules
* @author Marian Klühspies
*/
task embedProguard << {
println "====== Embedding proguard.txt files in " + project
def proguardRelease = file("build/intermediates/bundles/release/proguard.txt")
embeddedAarDirs.each { aarPath ->
try {
def proguardLibFile = file("$aarPath/proguard.txt")
if (proguardLibFile.exists())
proguardRelease.append(proguardLibFile.text)
} catch (Exception e) {
e.printStackTrace();
throw e;
}
}
}
/**
* To embed the class files, we simply explode them in the proper place. Android's packager will
* then take care of them and merge them into the classes.jar
*/
task embedJavaJars << {
embeddedAarDirs.each { aarPath ->
// Collect list of all jar files
FileTree jars = fileTree(dir: "$aarPath/jars", include: ['*.jar'])
jars += fileTree(dir: "$aarPath/jars/libs", include: ['*.jar']);
jars += fileTree(dir: "$aarPath", include: ['*.jar']);
jars += fileTree(dir: "$aarPath/libs", include: ['*.jar']);
// println "Embedding jars : " + jars
// Explode all jar files to classes so that they can be proguarded
jars.visit {
FileVisitDetails element ->
copy {
from(zipTree(element.file))
into("build/intermediates/classes/release/")
}
}
}
}
/**
* For some reason, adding to the jniLibs source set does not work. So we simply copy all files.
*/
task embedJniLibs << {
println "====== Embedding jni libs in " + project
embeddedAarDirs.each { aarPath ->
println "======= Copying JNI from $aarPath/jni"
// Copy JNI Folders
copy {
from fileTree(dir: "$aarPath/jni")
into file("build/intermediates/bundles/release/jni")
}
}
}
/**
* Embedding R.java from the embedded libraries is problematic since the constant value changes when
* included in the final app. For this reason, the app build process generates the R.class for each
* included package. We solve this by generating the R.java for the embedded package and mapping each
* entry to the fat library's value. This way, when the fat library's R.class is generated by the
* app, the embedded R.class automatically gets the correct value.
*/
task embedRClassFiles << {
def libPackageName = new XmlParser().parse(android.sourceSets.main.manifest.srcFile).@package
embeddedAarDirs.each { aarPath ->
def aarManifest = new XmlParser().parse(file("$aarPath/AndroidManifest.xml"));
def aarPackageName = aarManifest.@package
String packagePath = aarPackageName.replace('.', '/')
// Generate the R.java file and map to current project's R.java
// This will recreate the class file
def rTxt = file("$aarPath/R.txt")
def rMap = new ConfigObject()
if (rTxt.exists()) {
rTxt.eachLine {
line ->
def (type, subclass, name, value) = line.tokenize(' ')
rMap[subclass].putAt(name, type)
}
}
def sb = "package $aarPackageName;" << '\n' << '\n'
sb << 'public final class R {' << '\n'
rMap.each {
subclass, values ->
sb << " public static final class $subclass {" << '\n'
values.each {
name, type ->
sb << " public static $type $name = ${libPackageName}.R.${subclass}.${name};" << '\n'
}
sb << " }" << '\n'
}
sb << '}' << '\n'
// println sb
mkdir("build/generated/source/r/release/$packagePath")
file("build/generated/source/r/release/$packagePath/R.java").write(sb.toString())
}
}
task embedManifests << {
ILogger mLogger = new MiLogger()
List libraryManifests = new ArrayList<>()
embeddedAarDirs.each { aarPath ->
if (!libraryManifests.contains(aarPath)) {
libraryManifests.add(file("$aarPath/AndroidManifest.xml"))
// println "==== Added $aarPath Manifest"
}
}
File reportFile = file("build/embedManifestReport.txt")
File origManifest = file("build/intermediates/bundles/release/AndroidManifest.xml")
File copyManifest = file("build/intermediates/bundles/release/AndroidManifest.orig.xml")
File aaptManifest = file("build/intermediates/bundles/release/aapt/AndroidManifest.xml")
copy {
from origManifest.parentFile
into copyManifest.parentFile
include origManifest.name
rename(origManifest.name, copyManifest.name)
}
String outManifestLocation = origManifest.absolutePath
String outAaptSafeManifestLocation = aaptManifest.absolutePath
try {
Invoker manifestMergerInvoker = ManifestMerger2.newMerger(copyManifest, mLogger, MergeType.APPLICATION)
manifestMergerInvoker.addLibraryManifests(libraryManifests.toArray(new File[libraryManifests.size()]))
// manifestMergerInvoker.setPlaceHolderValues(placeHolders)
manifestMergerInvoker.setMergeReportFile(reportFile);
MergingReport mergingReport = manifestMergerInvoker.merge();
mLogger.info("Merging result:" + mergingReport.getResult());
MergingReport.Result result = mergingReport.getResult();
switch (result) {
case MergingReport.Result.WARNING:
mergingReport.log(mLogger);
// fall through since these are just warnings.
case MergingReport.Result.SUCCESS:
XmlDocument xmlDocument = mergingReport.getMergedDocument().get();
try {
String annotatedDocument = mergingReport.getActions().blame(xmlDocument);
mLogger.verbose(annotatedDocument);
} catch (Exception e) {
mLogger.error(e, "cannot print resulting xml");
}
save(xmlDocument, new File(outManifestLocation));
if (outAaptSafeManifestLocation != null) {
new PlaceholderEncoder().visit(xmlDocument);
save(xmlDocument, new File(outAaptSafeManifestLocation));
}
mLogger.info("Merged manifest saved to " + outManifestLocation);
break;
case MergingReport.Result.ERROR:
mergingReport.log(mLogger);
throw new RuntimeException(mergingReport.getReportString());
default:
throw new RuntimeException("Unhandled result type : " + mergingReport.getResult());
}
} catch (RuntimeException e) {
// TODO: unacceptable.
e.printStackTrace()
throw new RuntimeException(e);
}
}
private void save(XmlDocument xmlDocument, File out) {
try {
Files.write(xmlDocument.prettyPrint(), out, Charsets.UTF_8);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
class MiLogger implements ILogger {
@Override
void error(
@com.android.annotations.Nullable Throwable t,
@com.android.annotations.Nullable String msgFormat, Object... args) {
System.err.println(String.format("========== ERROR : " + msgFormat, args))
if (t) t.printStackTrace(System.err)
}
@Override
void warning(@NonNull String msgFormat, Object... args) {
System.err.println(String.format("========== WARNING : " + msgFormat, args))
}
@Override
void info(@NonNull String msgFormat, Object... args) {
System.out.println(String.format("========== INFO : " + msgFormat, args))
}
@Override
void verbose(@NonNull String msgFormat, Object... args) {
// System.out.println(String.format("========== DEBUG : " + msgFormat, args))
}
}