├── .gitignore ├── README.md ├── kobalt ├── src │ └── Build.kt └── wrapper │ ├── kobalt-wrapper.jar │ └── kobalt-wrapper.properties ├── kobaltw ├── kobaltw.bat └── src └── main ├── kotlin └── com │ └── beust │ └── kobalt │ └── plugin │ └── android │ ├── AarGenerator.kt │ ├── AndroidCommand.kt │ ├── AndroidConfig.kt │ ├── AndroidFiles.kt │ ├── AndroidManifestXml.kt │ ├── AndroidPlugin.kt │ ├── AppInfo.kt │ ├── KobaltResourceMerger.kt │ ├── Proguard.kt │ ├── RunCommand.kt │ ├── SdkDownloader.kt │ └── Templates.kt └── resources ├── META-INF └── kobalt-plugin.xml └── templates ├── androidJavaTemplate.jar └── androidKotlinTemplate.jar /.gitignore: -------------------------------------------------------------------------------- 1 | .gradle 2 | annotations 3 | .idea/* 4 | buildScript 5 | kobaltBuild 6 | local.properties 7 | classes 8 | libs 9 | .kobalt/ 10 | ./build/ 11 | out 12 | .DS_Store 13 | *.iml 14 | lib/kotlin-r*.jar 15 | 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Android plug-in for [Kobalt](http://beust.com/kobalt). 2 | 3 | # Features 4 | 5 | The Kobalt Android plug-in offers the following features: 6 | 7 | - Automatic SDK downloading 8 | - Resource merging 9 | - Manifest merging 10 | - Predexing 11 | - Generation of apk files 12 | - Install and run activities on the device 13 | 14 | It also supports features already offered by Kobalt itself: 15 | 16 | - `BuildConfig` 17 | - Incremental tasks 18 | 19 | The plug-in uses Google's Android build library, which guarantees that all these operations are being performed the same way as the official Gradle Android plug-in. This also makes it easier to add new functionalities as they get added to the official Android builder library. 20 | 21 | For a full example of a complex Android application being built with Kobalt, take a look at the [Kobalt u2020 fork](https://github.com/cbeust/u2020/blob/build-with-kobalt/kobalt/src/Build.kt). 22 | 23 | # SDK Management 24 | 25 | Kobalt will automatically everything that is necessary to build Android applications, including: 26 | 27 | - The Android SDK 28 | - Support libraries 29 | - Build tools 30 | - etc... 31 | 32 | The plug-in will start by looking if `$ANDROID_HOME` is defined and if it is, use it. It it's not defined, the SDK will 33 | be downloaded in `~/.android-sdk`. You can find more details about this feature [in this article](http://beust.com/weblog/2016/04/09/the-kobalt-diaries-automatic-android-sdk-management/). 34 | 35 | # Build file 36 | 37 | You introduce Android inside your project by including the Kobalt Android plug-in: 38 | 39 | ``` 40 | val bs = buildScript { 41 | // ... 42 | plugins("com.beust:kobalt-android:$kobaltAndroid") 43 | } 44 | ``` 45 | 46 | Import the symbols: 47 | 48 | ``` 49 | import com.beust.kobalt.plugin.android.* 50 | ``` 51 | 52 | You can now use the `android` directive inside your project: 53 | 54 | ``` 55 | val p = project { 56 | // ... 57 | 58 | android { 59 | defaultConfig { 60 | minSdkVersion = 15 61 | versionCode = 100 62 | versionName = "1.0.0" 63 | compileSdkVersion = "23" 64 | buildToolsVersion = "23.0.1" 65 | applicationId = "com.beust.myapp" 66 | } 67 | } 68 | ``` 69 | 70 | These fields should look familiar to Android developers. The `defaultConfig` directive defines values that will apply to all variants of your build, but each variant can override any of these values. 71 | 72 | # Tasks 73 | 74 | The Kobalt Android plug-in introduces a few new tasks (you can get the full list with `./kobaltw --tasks` with the plug-in enabled). The ones you will likely use the most often are: 75 | 76 | - `install`. Build and install the apk on the device. 77 | - `run`. Run the main activity of your application on the device. 78 | 79 | The plug-in also creates tasks for each of the variants found in your build file, e.g. `installProductionDebug`, `runInternalRelease`, etc... 80 | 81 | # Libraries 82 | 83 | By default, an Android project will output an apk but you can request an aar file to be produced with the `aar{}` 84 | directive: 85 | 86 | ``` 87 | val p = project { 88 | // ... 89 | 90 | android { 91 | aar { 92 | } 93 | } 94 | } 95 | ``` 96 | 97 | This directive lets you specify additional configuration parameters such as the `name` of the aar file to produce. Refer to the documentation of the `AarConfig` class for more details. 98 | -------------------------------------------------------------------------------- /kobalt/src/Build.kt: -------------------------------------------------------------------------------- 1 | 2 | import com.beust.kobalt.file 3 | import com.beust.kobalt.homeDir 4 | import com.beust.kobalt.plugin.kotlin.kotlinCompiler 5 | import com.beust.kobalt.plugin.packaging.assemble 6 | import com.beust.kobalt.plugin.publish.bintray 7 | import com.beust.kobalt.project 8 | 9 | val dev = false 10 | val kobalt = "com.beust:kobalt-plugin-api:1.0.8" 11 | val kobaltDev = file(homeDir("kotlin/kobalt/kobaltBuild/libs/kobalt-1.0.13.jar")) 12 | 13 | val p = project { 14 | name = "kobalt-android" 15 | artifactId = name 16 | group = "com.beust" 17 | version = "0.98" 18 | 19 | dependencies { 20 | // provided("org.jetbrains:") 21 | compile("com.android.tools.build:builder:2.0.0-alpha3", 22 | "org.rauschig:jarchivelib:0.7.1") 23 | 24 | // Kobalt dependencies depending on whether I'm debugging or releasing. 25 | // To release, depend on "kobalt-plugin-api". For development, depend on "kobalt", which 26 | // provides a com.beust.kobalt.main() function so you can start Kobalt loaded with your 27 | // plug-in directly from your main plug-in class. 28 | compile(if (dev) kobaltDev else kobalt) 29 | } 30 | 31 | assemble { 32 | mavenJars { 33 | fatJar = true 34 | } 35 | } 36 | 37 | bintray { 38 | publish = true 39 | } 40 | 41 | kotlinCompiler { 42 | args("-nowarn") 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /kobalt/wrapper/kobalt-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cbeust/kobalt-android/e69a25c6146e544de81bfc85ceacff238d18e953/kobalt/wrapper/kobalt-wrapper.jar -------------------------------------------------------------------------------- /kobalt/wrapper/kobalt-wrapper.properties: -------------------------------------------------------------------------------- 1 | kobalt.version=1.0.83 -------------------------------------------------------------------------------- /kobaltw: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | java -jar "`dirname "$0"`/kobalt/wrapper/kobalt-wrapper.jar" $* 3 | -------------------------------------------------------------------------------- /kobaltw.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | set DIRNAME=%~dp0 3 | if "%DIRNAME%" == "" set DIRNAME=. 4 | java -jar "%DIRNAME%/kobalt/wrapper/kobalt-wrapper.jar" %* 5 | -------------------------------------------------------------------------------- /src/main/kotlin/com/beust/kobalt/plugin/android/AarGenerator.kt: -------------------------------------------------------------------------------- 1 | package com.beust.kobalt.plugin.android 2 | 3 | import com.beust.kobalt.IFileSpec 4 | import com.beust.kobalt.JarGenerator 5 | import com.beust.kobalt.api.KobaltContext 6 | import com.beust.kobalt.api.Project 7 | import com.beust.kobalt.archive.Archives 8 | import com.beust.kobalt.archive.Jar 9 | import com.beust.kobalt.maven.DependencyManager 10 | import com.beust.kobalt.misc.From 11 | import com.beust.kobalt.misc.IncludedFile 12 | import com.beust.kobalt.misc.To 13 | import com.beust.kobalt.misc.log 14 | import com.google.inject.Inject 15 | import java.io.File 16 | 17 | /** 18 | * Generates a .aar file based on http://tools.android.com/tech-docs/new-build-system/aar-format 19 | */ 20 | class AarGenerator @Inject constructor(val jarGenerator: JarGenerator, val dependencyManager: DependencyManager) { 21 | fun generate(project: Project, context: KobaltContext, config: AarConfig) { 22 | println("Creating aar for " + config) 23 | 24 | val variant = context.variant 25 | val archiveName = config.name ?: "${project.name}-${project.version}.aar" 26 | 27 | val includedFiles = arrayListOf() 28 | 29 | // AndroidManifest.xml 30 | val manifest = AndroidFiles.mergedManifest(project, variant) 31 | includedFiles.add(IncludedFile(From(File(manifest).parent), To(""), 32 | listOf(IFileSpec.FileSpec("AndroidManifest.xml")))) 33 | 34 | // classes.jar 35 | val jar = Jar(project, "classes.jar", fatJar = false).apply { 36 | include(From(AndroidFiles.intermediates(project)), To(""), IFileSpec.GlobSpec("**/*.class")) 37 | } 38 | val file = jarGenerator.generateJar(project, context, jar) 39 | includedFiles.add(IncludedFile(From(file.parentFile.path), To(""), 40 | listOf(IFileSpec.FileSpec(file.name)))) 41 | 42 | // res 43 | includedFiles.add(IncludedFile(From(AndroidFiles.mergedResources(project, variant)), To("res"), 44 | listOf(IFileSpec.GlobSpec("**/*")))) 45 | 46 | // R.txt 47 | includedFiles.add(IncludedFile(From(AndroidFiles.rTxtDir), To(""), 48 | listOf(IFileSpec.FileSpec(AndroidFiles.rTxtName)))) 49 | 50 | // assets 51 | includedFiles.add(IncludedFile(From(AndroidFiles.assets(project)), To("assets"), 52 | listOf(IFileSpec.GlobSpec("**/*")))) 53 | 54 | // libs/*.jar 55 | val dependencies = dependencyManager.transitiveClosure(project.compileDependencies) 56 | dependencies.map { it.jarFile.get() }.filter { 57 | it.name.endsWith(".jar") && it.name != "classes.jar" && it.name != "android.jar" 58 | }.forEach { jar -> 59 | includedFiles.add(IncludedFile(From(jar.parentFile.path), To("libs"), 60 | listOf(IFileSpec.FileSpec(jar.name)))) 61 | } 62 | 63 | // jni 64 | includedFiles.add(IncludedFile(From("jni"), To("jni"), listOf(IFileSpec.GlobSpec("**/*")))) 65 | 66 | // proguard.txt, lint.jar 67 | includedFiles.add(IncludedFile(listOf("proguard.txt", "lint.jar").filter { 68 | File(it).exists() 69 | }.map { 70 | IFileSpec.FileSpec(it) 71 | })) 72 | 73 | val aar = Archives.generateArchive(project, context, archiveName, ".aar", includedFiles) 74 | log(1, "Generated " + aar.path) 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/main/kotlin/com/beust/kobalt/plugin/android/AndroidCommand.kt: -------------------------------------------------------------------------------- 1 | package com.beust.kobalt.plugin.android 2 | 3 | import com.beust.kobalt.api.Project 4 | import com.beust.kobalt.misc.RunCommand 5 | import com.beust.kobalt.misc.log 6 | import java.io.File 7 | 8 | open class AndroidCommand(project: Project, androidHome: String, 9 | command: String, cwd: File = File(project.directory)) : RunCommand(command) { 10 | init { 11 | env.put("ANDROID_HOME", androidHome) 12 | directory = cwd 13 | } 14 | 15 | open fun call(args: List) = run(args, 16 | successCallback = { output -> 17 | log(1, "$command succeeded:") 18 | output.forEach { 19 | log(1, " $it") 20 | } 21 | }, 22 | errorCallback = { output -> 23 | with(StringBuilder()) { 24 | append("Error running $command:") 25 | output.forEach { 26 | append(" $it") 27 | } 28 | error(this.toString()) 29 | } 30 | }) 31 | } 32 | 33 | 34 | -------------------------------------------------------------------------------- /src/main/kotlin/com/beust/kobalt/plugin/android/AndroidConfig.kt: -------------------------------------------------------------------------------- 1 | package com.beust.kobalt.plugin.android 2 | 3 | import com.beust.kobalt.api.Project 4 | import com.beust.kobalt.api.annotation.Directive 5 | 6 | class AndroidConfig(val project: Project, 7 | var compileSdkVersion : String? = null, 8 | var buildToolsVersion: String? = null, 9 | var applicationId: String? = null, 10 | val androidHome: String? = null) { 11 | 12 | val signingConfigs = hashMapOf() 13 | 14 | fun addSigningConfig(name: String, project: Project, signingConfig: SigningConfig) { 15 | signingConfigs.put(name, signingConfig) 16 | } 17 | 18 | var defaultConfig: DefaultConfig = DefaultConfig() 19 | 20 | @Directive 21 | fun defaultConfig(init: DefaultConfig.() -> Unit) { 22 | defaultConfig = DefaultConfig().apply { init() } 23 | } 24 | } 25 | 26 | -------------------------------------------------------------------------------- /src/main/kotlin/com/beust/kobalt/plugin/android/AndroidFiles.kt: -------------------------------------------------------------------------------- 1 | package com.beust.kobalt.plugin.android 2 | 3 | import com.beust.kobalt.Variant 4 | import com.beust.kobalt.api.KobaltContext 5 | import com.beust.kobalt.api.Project 6 | import com.beust.kobalt.maven.MavenId 7 | import com.beust.kobalt.misc.KFiles 8 | 9 | class AndroidFiles { 10 | companion object { 11 | fun intermediates(project: Project) = KFiles.joinDir(project.directory, KFiles.KOBALT_BUILD_DIR, 12 | "intermediates", "classes") 13 | 14 | fun assets(project: Project) = KFiles.joinDir(intermediates(project), "assets") 15 | 16 | fun manifest(project: Project) = 17 | KFiles.joinDir(project.directory, "src", "main", "AndroidManifest.xml") 18 | 19 | fun mergedManifest(project: Project, variant: Variant) : String { 20 | val dir = KFiles.joinAndMakeDir(intermediates(project), "manifests", "full", variant.toIntermediateDir()) 21 | return KFiles.joinDir(dir, "AndroidManifest.xml") 22 | } 23 | 24 | fun generatedSource(project: Project, context: KobaltContext) = KFiles.joinAndMakeDir(project.directory, 25 | project.buildDirectory, "generated", "source", "aidl", context.variant.toIntermediateDir()) 26 | 27 | fun mergedResourcesNoVariant(project: Project) = 28 | KFiles.joinAndMakeDir(intermediates(project), "res", "merged") 29 | 30 | fun mergedResources(project: Project, variant: Variant) = 31 | KFiles.joinAndMakeDir(mergedResourcesNoVariant(project), variant.toIntermediateDir()) 32 | 33 | fun exploded(project: Project, mavenId: MavenId) = KFiles.joinAndMakeDir( 34 | intermediates(project), "exploded-aar", mavenId.groupId, mavenId.artifactId, mavenId.version!!) 35 | 36 | fun explodedManifest(project: Project, mavenId: MavenId) = 37 | KFiles.joinDir(exploded(project, mavenId), "AndroidManifest.xml") 38 | 39 | fun aarClassesJar(dir: String) = KFiles.joinDir(dir, "classes.jar") 40 | 41 | fun apk(project: Project, flavor: String) 42 | = KFiles.joinFileAndMakeDir(project.directory, project.buildDirectory, "outputs", "apk", 43 | "${project.name}$flavor.apk") 44 | 45 | fun explodedClassesJar(project: Project, mavenId: MavenId) = aarClassesJar( 46 | KFiles.joinDir(exploded(project, mavenId))) 47 | 48 | fun temporaryApk(project: Project, flavor: String) 49 | = KFiles.joinFileAndMakeDir(AndroidFiles.intermediates(project), "res", "resources$flavor.ap_") 50 | 51 | /** The R.txt directory */ 52 | val rTxtDir = KFiles.joinDir(KFiles.KOBALT_BUILD_DIR, "symbols") 53 | 54 | val rTxtName = "R.txt" 55 | 56 | fun preDexed(project: Project, variant: Variant) = 57 | KFiles.joinAndMakeDir(intermediates(project), "pre-dexed", variant.toIntermediateDir()) 58 | 59 | fun intermediatesClasses(project: Project, context: KobaltContext) 60 | = KFiles.joinAndMakeDir(intermediates(project), "classes", context.variant.toIntermediateDir()) 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/main/kotlin/com/beust/kobalt/plugin/android/AndroidManifestXml.kt: -------------------------------------------------------------------------------- 1 | package com.beust.kobalt.plugin.android 2 | 3 | import java.io.InputStream 4 | import javax.xml.bind.JAXBContext 5 | import javax.xml.bind.annotation.XmlAttribute 6 | import javax.xml.bind.annotation.XmlElement 7 | import javax.xml.bind.annotation.XmlRootElement 8 | 9 | /** 10 | * Parse AndroidManifest.xml and expose its content. 11 | */ 12 | class AndroidManifest(val ins: InputStream) { 13 | companion object { 14 | const val NAMESPACE = "http://schemas.android.com/apk/res/android" 15 | } 16 | 17 | val manifest: AndroidManifestXml by lazy { 18 | val jaxbContext = JAXBContext.newInstance(AndroidManifestXml::class.java) 19 | jaxbContext.createUnmarshaller().unmarshal(ins) as AndroidManifestXml 20 | } 21 | 22 | val pkg by lazy { 23 | manifest.pkg 24 | } 25 | 26 | val mainActivity: String? by lazy { 27 | fun isLaunch(act: ActivityXml) : Boolean { 28 | val r = act.intentFilters.filter { inf: IntentFilter -> 29 | inf.action?.name == "android.intent.action.MAIN" && 30 | inf.category?.name == "android.intent.category.LAUNCHER" 31 | } 32 | return r.size > 0 33 | } 34 | val act = manifest.application?.activities?.filter { isLaunch(it) } 35 | if (act != null && act.size > 0) { 36 | act.get(0).name?.let { n -> 37 | if (n.startsWith(".")) pkg + "." + n.substring(1) else n 38 | } 39 | } else { 40 | null 41 | } 42 | } 43 | } 44 | 45 | @XmlRootElement(name = "manifest") 46 | class AndroidManifestXml { 47 | @XmlAttribute(name = "package") @JvmField 48 | val pkg: String? = null 49 | var application: ApplicationXml? = null 50 | } 51 | 52 | class ApplicationXml { 53 | @XmlElement(name = "activity") @JvmField 54 | var activities: List = arrayListOf() 55 | } 56 | 57 | class ActivityXml { 58 | @XmlAttribute(namespace = AndroidManifest.NAMESPACE, name = "name") @JvmField 59 | var name: String? = null 60 | 61 | @XmlElement(name = "intent-filter") @JvmField 62 | var intentFilters: List = arrayListOf() 63 | } 64 | 65 | class IntentFilter { 66 | var action: ActionXml? = null 67 | var category: CategoryXml? = null 68 | } 69 | 70 | class ActionXml { 71 | @XmlAttribute(namespace = AndroidManifest.NAMESPACE, name = "name") @JvmField 72 | var name: String? = null 73 | } 74 | 75 | class CategoryXml { 76 | @XmlAttribute(namespace = AndroidManifest.NAMESPACE, name = "name") @JvmField 77 | var name: String? = null 78 | } 79 | -------------------------------------------------------------------------------- /src/main/kotlin/com/beust/kobalt/plugin/android/AndroidPlugin.kt: -------------------------------------------------------------------------------- 1 | package com.beust.kobalt.plugin.android 2 | 3 | import com.beust.kobalt.* 4 | import com.beust.kobalt.api.* 5 | import com.beust.kobalt.api.annotation.Directive 6 | import com.beust.kobalt.api.annotation.ExportedProjectProperty 7 | import com.beust.kobalt.api.annotation.IncrementalTask 8 | import com.beust.kobalt.api.annotation.Task 9 | import com.beust.kobalt.maven.DependencyManager 10 | import com.beust.kobalt.maven.MavenId 11 | import com.beust.kobalt.maven.Md5 12 | import com.beust.kobalt.maven.dependency.FileDependency 13 | import com.beust.kobalt.misc.* 14 | import com.google.common.collect.HashMultimap 15 | import com.google.inject.Inject 16 | import com.google.inject.Singleton 17 | import java.io.File 18 | import java.io.FileInputStream 19 | import java.nio.file.Path 20 | import java.nio.file.Paths 21 | import java.util.* 22 | 23 | /** 24 | * The Android plug-in which executes: 25 | * library dependencies (android.library.reference.N) 26 | * ndk 27 | * aidl 28 | * renderscript 29 | * BuildConfig.java 30 | * aapt 31 | * compile 32 | * obfuscate 33 | * dex 34 | * png crunch 35 | * package resources 36 | * package apk 37 | * sign 38 | * zipalign 39 | */ 40 | @Singleton 41 | class AndroidPlugin @Inject constructor(val dependencyManager: DependencyManager, 42 | val taskContributor : TaskContributor, val aarGenerator: AarGenerator) 43 | : BasePlugin(), IConfigActor, IClasspathContributor, IRepoContributor, 44 | ICompilerFlagContributor, ICompilerInterceptor, IBuildDirectoryInterceptor, IRunnerContributor, 45 | IClasspathInterceptor, ISourceDirectoryContributor, IBuildConfigFieldContributor, ITaskContributor, 46 | IMavenIdInterceptor, ICompilerContributor, ITemplateContributor, IAssemblyContributor { 47 | 48 | // 49 | // Android homes per project 50 | // 51 | private val androidHomes = hashMapOf() 52 | private fun androidHome(project: Project) : String { 53 | return androidHomes.computeIfAbsent(project.name, { 54 | val config = configurationFor(project) 55 | if (config != null) { 56 | SdkUpdater(config.androidHome, config.compileSdkVersion, config.buildToolsVersion).maybeInstall(project) 57 | } else { 58 | throw KobaltException("Should never happen") 59 | } 60 | }) 61 | } 62 | 63 | // 64 | // Resource mergers per project 65 | // 66 | private val resourceMergers = hashMapOf() 67 | private fun resourceMerger(project: Project) 68 | = resourceMergers.computeIfAbsent(project.name, { KobaltResourceMerger(androidHome(project)) }) 69 | 70 | override val configurations : HashMap = hashMapOf() 71 | 72 | private val IDL_COMPILER = object: ICompiler { 73 | override fun compile(project: Project, context: KobaltContext, info: CompilerActionInfo): TaskResult { 74 | val version = configurationFor(project)?.compileSdkVersion 75 | val pArg = "-p" + androidHome(project) + "/platforms/android-$version/framework.aidl" 76 | val oArg = "-o" + AndroidFiles.generatedSource(project, context) 77 | val exp = explodedAarDirectories(project).map { it.second } 78 | val included = exp.map { 79 | "-I" + KFiles.joinDir(it.path, "aidl") 80 | } 81 | val success = runCommand { 82 | command = aidl(project) 83 | args = listOf(pArg) + listOf(oArg) + included + info.sourceFiles 84 | directory = if (info.directory == "") File(".") else File(info.directory) 85 | 86 | } 87 | return TaskResult(if (success == 0) true else false) 88 | } 89 | } 90 | 91 | private val IDL_COMPILER_DESCRIPTION = CompilerDescription( 92 | "IDL", 93 | sourceSuffixes = listOf("aidl", "idl"), 94 | sourceDirectory = "idl", 95 | compiler = IDL_COMPILER) 96 | 97 | /* 98 | /Users/beust/android/adt-bundle-mac-x86_64-20140702/sdk/build-tools/23.0.2/aidl -p/Users/beust/android/adt-bundle-mac-x86_64-20140702/sdk/platforms/android-23/framework.aidl -o/Users/beust/t/MaterialAudiobookPlayer/audiobook/build/generated/source/aidl/debug -I/Users/beust/t/MaterialAudiobookPlayer/audiobook/src/main/aidl -I/Users/beust/t/MaterialAudiobookPlayer/audiobook/src/debug/aidl -I/Users/beust/t/MaterialAudiobookPlayer/audiobook/build/intermediates/exploded-aar/com.afollestad.material-dialogs/core/0.8.5.3/aidl -I/Users/beust/t/MaterialAudiobookPlayer/audiobook/build/intermediates/exploded-aar/me.zhanghai.android.materialprogressbar/library/1.1.4/aidl -I/Users/beust/t/MaterialAudiobookPlayer/audiobook/build/intermediates/exploded-aar/com.android.support/recyclerview-v7/23.1.1/aidl -I/Users/beust/t/MaterialAudiobookPlayer/audiobook/build/intermediates/exploded-aar/com.android.support/appcompat-v7/23.1.1/aidl -I/Users/beust/t/MaterialAudiobookPlayer/audiobook/build/intermediates/exploded-aar/com.getbase/floatingactionbutton/1.10.1/aidl -I/Users/beust/t/MaterialAudiobookPlayer/audiobook/build/intermediates/exploded-aar/com.android.support/support-v4/23.1.1/aidl -I/Users/beust/t/MaterialAudiobookPlayer/audiobook/build/intermediates/exploded-aar/com.jakewharton.timber/timber/4.1.0/aidl -I/Users/beust/t/MaterialAudiobookPlayer/audiobook/build/intermediates/exploded-aar/com.github.JakeWharton.RxBinding/rxbinding-kotlin/542cd7e8a4/aidl -I/Users/beust/t/MaterialAudiobookPlayer/audiobook/build/intermediates/exploded-aar/com.github.JakeWharton.RxBinding/rxbinding/542cd7e8a4/aidl -I/Users/beust/t/MaterialAudiobookPlayer/audiobook/build/intermediates/exploded-aar/io.reactivex/rxandroid/1.1.0/aidl -I/Users/beust/t/MaterialAudiobookPlayer/audiobook/build/intermediates/exploded-aar/com.squareup.leakcanary/leakcanary-android/1.4-beta1/aidl -d/var/folders/77/kjr_lq4x5tj5ymxdfvxs6c7c002p8z/T/aidl768872507241036042.d /Users/beust/t/MaterialAudiobookPlayer/audiobook/src/main/aidl/com/android/vending/billing/IInAppBillingService.aidl 99 | */ 100 | 101 | override fun compilersFor(project: Project, context: KobaltContext): List = listOf(IDL_COMPILER_DESCRIPTION) 102 | 103 | companion object { 104 | const val PLUGIN_NAME = "Android" 105 | const val TASK_GENERATE_DEX = "generateDex" 106 | const val TASK_SIGN_APK = "signApk" 107 | const val TASK_INSTALL= "install" 108 | 109 | @ExportedProjectProperty(doc = "The location of the produced apk") 110 | const val PROJECT_PROPERTY_APK_PATH = "apkPath" 111 | } 112 | 113 | override val name = PLUGIN_NAME 114 | 115 | fun isAndroid(project: Project) = configurationFor(project) != null 116 | 117 | override fun shutdown() { 118 | resourceMergers.values.forEach { it.shutdown() } 119 | } 120 | 121 | override fun apply(project: Project, context: KobaltContext) { 122 | super.apply(project, context) 123 | val config = configurationFor(project) 124 | if (config != null) { 125 | classpathEntries.put(project.name, FileDependency(androidJar(project).toString())) 126 | project.compileDependencies.filter { it.jarFile.get().path.endsWith("jar")}.forEach { 127 | classpathEntries.put(project.name, FileDependency(it.jarFile.get().path)) 128 | } 129 | 130 | taskContributor.addVariantTasks(this, project, context, "generateR", runBefore = listOf("compile"), 131 | runTask = { taskGenerateRFile(project) }) 132 | taskContributor.addIncrementalVariantTasks(this, project, context, "generateDex", 133 | runAfter = listOf ("compile"), 134 | runBefore = listOf("assemble"), 135 | runTask = { taskGenerateDex(project) }) 136 | taskContributor.addVariantTasks(this, project, context, "signApk", runAfter = listOf("generateDex"), 137 | runBefore = listOf("assemble"), 138 | runTask = { taskSignApk(project) }) 139 | taskContributor.addVariantTasks(this, project, context, "install", runAfter = listOf("signApk"), 140 | runTask = { taskInstall(project) }) 141 | taskContributor.addVariantTasks(this, project, context, "proguard", runBefore = listOf("install"), 142 | runAfter = listOf("compile"), 143 | runTask = { taskProguard(project) }) 144 | } 145 | // context.pluginInfo.classpathContributors.add(this) 146 | } 147 | 148 | 149 | override fun accept(project: Project) = isAndroid(project) 150 | 151 | fun compileSdkVersion(project: Project) = configurationFor(project)?.compileSdkVersion 152 | 153 | fun buildToolsVersion(project: Project): String { 154 | val version = configurationFor(project)?.buildToolsVersion 155 | if (OperatingSystem.current().isWindows() && version == "21.1.2") 156 | return "build-tools-$version" 157 | else 158 | return version as String 159 | } 160 | 161 | fun androidJar(project: Project): Path = 162 | Paths.get(androidHome(project), "platforms", "android-${compileSdkVersion(project)}", "android.jar") 163 | 164 | private fun buildToolCommand(project: Project, command: String) 165 | = "${androidHome(project)}/build-tools/${buildToolsVersion(project)}/$command" 166 | 167 | private fun aapt(project: Project) = buildToolCommand(project, "aapt") 168 | 169 | private fun aidl(project: Project) = buildToolCommand(project, "aidl") 170 | 171 | private fun adb(project: Project) = "${androidHome(project)}/platform-tools/adb" 172 | 173 | private val preDexFiles = arrayListOf() 174 | 175 | @Task(name = "generateR", description = "Generate the R.java file", // runAfter = arrayOf("clean"), 176 | reverseDependsOn = arrayOf ("compile")) 177 | fun taskGenerateRFile(project: Project): TaskResult { 178 | 179 | val aarDependencies = explodeAarFiles(project) 180 | preDexFiles.addAll(preDex(project, context.variant, aarDependencies)) 181 | val extraSourceDirs = resourceMerger(project) 182 | .run(project, context.variant, configurationFor(project)!!, aarDependencies.directories) 183 | extraSourceDirectories.addAll(extraSourceDirs.map { File(it) }) 184 | 185 | return TaskResult(true) 186 | } 187 | 188 | /** 189 | * Predex all the libraries that need to be predexed then return a list of them. 190 | */ 191 | private fun preDex(project: Project, variant: Variant, aarInfo: AarInfo) : List { 192 | log(2, "Predexing") 193 | val result = arrayListOf() 194 | val aarFiles = aarInfo.jarFiles 195 | val jarFiles = dependencies(project).filter { !isAar(it) }.map { File(it) } 196 | 197 | val allDependencies = (aarFiles + jarFiles).toHashSet().filter { 198 | it.path.endsWith("jar") && it.exists() 199 | } 200 | 201 | allDependencies.forEach { dep -> 202 | val versionFile = File(dep.path).parentFile 203 | val artifactFile = versionFile.parentFile 204 | val name = (if (artifactFile != null) artifactFile.name else "") + "-" + versionFile.name 205 | val outputDir = AndroidFiles.preDexed(project, variant) 206 | val outputFile = File(outputDir, name + ".jar") 207 | if (! outputFile.exists()) { 208 | log(2, " Predexing $dep") 209 | if (runDex(project, outputFile.path, target = dep.path)) { 210 | if (outputFile.exists()) result.add(outputFile.path) 211 | } else { 212 | log(2, "Dex command failed") 213 | } 214 | } else { 215 | log(2, " $dep already predexed") 216 | result.add(outputFile.path) 217 | } 218 | } 219 | return result 220 | } 221 | 222 | /** 223 | * aapt returns 0 even if it fails, so in order to detect whether it failed, we are checking 224 | * if its error stream contains anything. 225 | */ 226 | inner class AaptCommand(project: Project, aapt: String, val aaptCommand: String, 227 | cwd: File = File(".")) : AndroidCommand(project, androidHome(project), aapt) { 228 | init { 229 | directory = cwd 230 | useErrorStreamAsErrorIndicator = true 231 | } 232 | 233 | override fun call(args: List) = super.run(arrayListOf(aaptCommand) + args, 234 | errorCallback = { l: List -> println("ERRORS: $l")}, 235 | successCallback = { l: List -> }) 236 | } 237 | 238 | private fun explodedAarDirectories(project: Project) : List> { 239 | val result = 240 | if (project.dependencies != null) { 241 | val transitiveDependencies = dependencyManager.calculateDependencies(project, context, 242 | passedDependencies = project.dependencies!!.dependencies) 243 | transitiveDependencies.filter { it.isMaven && isAar(MavenId.create(it.id)) } 244 | .map { 245 | Pair(it, File(AndroidFiles.exploded(project, MavenId.create(it.id)))) 246 | } 247 | } else { 248 | emptyList() 249 | } 250 | 251 | return result 252 | } 253 | 254 | class AarInfo(val directories: List, val jarFiles: List, 255 | val dependencies: List) 256 | 257 | /** 258 | * Extract all the .aar files found in the dependencies and add their android.jar to classpathEntries, 259 | * which will be added to the classpath at compile time via the classpath interceptor. 260 | * @return all the jar files that need to be added to the classpath 261 | */ 262 | private fun explodeAarFiles(project: Project) : AarInfo { 263 | val jarFiles = arrayListOf() 264 | val directories = arrayListOf() 265 | val dependencies = arrayListOf() 266 | log(2, "Exploding aars") 267 | 268 | explodedAarDirectories(project).forEach { pair -> 269 | val (dep, destDir) = pair 270 | directories.add(destDir) 271 | dependencies.add(dep) 272 | val mavenId = MavenId.create(dep.id) 273 | if (!File(AndroidFiles.explodedManifest(project, mavenId)).exists()) { 274 | log(2, " Exploding ${dep.jarFile.get()} to $destDir") 275 | JarUtils.extractJarFile(dep.jarFile.get(), destDir) 276 | } else { 277 | log(2, " $destDir already exists, not extracting again") 278 | } 279 | val classesJar = AndroidFiles.explodedClassesJar(project, mavenId) 280 | 281 | // Add the classses.jar of this .aar to the classpath entries (which are returned via IClasspathContributor) 282 | classpathEntries.put(project.name, FileDependency(classesJar)) 283 | // Also add all the jar files found in the libs/ directory 284 | File(destDir, "libs").let { libsDir -> 285 | if (libsDir.exists()) { 286 | libsDir.listFiles().filter { it.name.endsWith(".jar") }.forEach { 287 | val libJarFile = FileDependency(it.absolutePath) 288 | classpathEntries.put(project.name, libJarFile) 289 | } 290 | } 291 | } 292 | jarFiles.add(File(AndroidFiles.aarClassesJar(destDir.path))) 293 | } 294 | return AarInfo(directories, jarFiles, dependencies) 295 | } 296 | 297 | /** 298 | * Implements ICompilerFlagContributor 299 | * Make sure we compile and generate 1.6 sources unless the build file defined those (which can 300 | * happen if the developer is using RetroLambda for example). 301 | */ 302 | override fun compilerFlagsFor(project: Project, context: KobaltContext, currentFlags: List, 303 | suffixesBeingCompiled: List) : List { 304 | if (isAndroid(project) && suffixesBeingCompiled.contains("java")) { 305 | var found = currentFlags.any { it == "-source" || it == "-target" } 306 | val result = arrayListOf().apply { addAll(currentFlags) } 307 | if (! found) { 308 | result.add("-source") 309 | result.add("1.6") 310 | result.add("-target") 311 | result.add("1.6") 312 | result.add("-nowarn") 313 | } 314 | return result 315 | } else { 316 | return emptyList() 317 | } 318 | } 319 | 320 | @Task(name = "proguard", description = "Run Proguard, if enabled", runBefore = arrayOf(TASK_GENERATE_DEX), 321 | dependsOn = arrayOf("compile")) 322 | fun taskProguard(project: Project): TaskResult { 323 | val config = configurationFor(project) 324 | if (config != null) { 325 | val buildType = context.variant.buildType 326 | if (buildType.minifyEnabled) { 327 | log(1, "minifyEnabled is true, running Proguard (not implemented yet)") 328 | // val classesDir = project.classesDir(context) 329 | // val proguardHome = KFiles.joinDir(androidHome(project), "tools", "proguard") 330 | // val proguardCommand = KFiles.joinDir(proguardHome, "bin", "proguard.sh") 331 | } 332 | } 333 | return TaskResult() 334 | } 335 | 336 | private fun dependencies(project: Project) = dependencyManager.calculateDependencies(project, 337 | context, 338 | passedDependencies = project.compileDependencies).map { 339 | it.jarFile.get().path 340 | }.filterNot { 341 | it.contains("android.jar") || it.endsWith(".aar") || it.contains("retrolambda") 342 | }.toHashSet().toTypedArray() 343 | 344 | class DexCommand : RunCommand("java") { 345 | override fun isSuccess(callSucceeded: Boolean, input: List, error: List) = 346 | error.size == 0 347 | } 348 | 349 | private fun inputChecksum(classDirectory: String) = Md5.toMd5Directories(listOf(File(classDirectory))) 350 | 351 | /** 352 | * @return true if dex succeeded 353 | */ 354 | private fun runDex(project: Project, outputJarFile: String, 355 | dependencies: List = emptyList(), 356 | target: String) : Boolean { 357 | // DexProcessBuilder(File(jarFile)). 358 | val args = arrayListOf( 359 | "-cp", KFiles.joinDir(androidHome(project), "build-tools", buildToolsVersion(project), "lib", "dx.jar"), 360 | "com.android.dx.command.Main", 361 | "--dex", 362 | "--num-threads=4", 363 | "--output", outputJarFile) 364 | if (KobaltLogger.LOG_LEVEL == 3) { 365 | args.add("--verbose") 366 | } 367 | var hasFiles = false 368 | 369 | val addedPredexFiles = hashSetOf() 370 | if (preDexFiles.size > 0) { 371 | val files = preDexFiles.filter { File(it).exists() && it.startsWith(project.directory) } 372 | args.addAll(files) 373 | addedPredexFiles.addAll(files.map { File(it).name }) 374 | hasFiles = true 375 | } 376 | 377 | // Only add dependencies that haven't been added in predex form 378 | val filteredDependencies = dependencies.map { it.jarFile.get() }.filter { 379 | ! it.path.endsWith("aar") && ! it.name.contains("android.jar") && it.name != "classes.jar" 380 | && ! addedPredexFiles.contains(it.name) 381 | } 382 | filteredDependencies.forEach { 383 | args.add(it.path) 384 | hasFiles = true 385 | } 386 | if (dependencies.isEmpty()) { 387 | if (File(target).isDirectory) { 388 | val classFiles = KFiles.findRecursively(File(target), { f -> f.endsWith(".class") }) 389 | if (classFiles.size > 0) { 390 | args.add(target) 391 | hasFiles = true 392 | } 393 | } else { 394 | args.add(target) 395 | hasFiles = true 396 | } 397 | } 398 | 399 | val exitCode = if (hasFiles) DexCommand().run(args) else 0 400 | return exitCode == 0 401 | } 402 | 403 | @IncrementalTask(name = TASK_GENERATE_DEX, description = "Generate the dex file", runBefore = arrayOf("assemble"), 404 | runAfter = arrayOf("compile")) 405 | fun taskGenerateDex(project: Project): IncrementalTaskInfo { 406 | File(project.classesDir(context)).mkdirs() 407 | return IncrementalTaskInfo( 408 | inputChecksum = { inputChecksum(project.classesDir(context)) }, 409 | outputChecksum = { null }, // TODO: return real checksum 410 | task = { project -> doTaskGenerateDex(project) }, 411 | context = context 412 | ) 413 | } 414 | 415 | fun doTaskGenerateDex(project: Project): TaskResult { 416 | // Don't run dex if we're producing an aar 417 | val runDex = aars[project.name] == null 418 | if (runDex) { 419 | // 420 | // Call dx to generate classes.dex 421 | // 422 | val classesDexDir = KFiles.joinDir(AndroidFiles.intermediates(project), "dex", 423 | context.variant.toIntermediateDir()) 424 | File(classesDexDir).mkdirs() 425 | val classesDex = "classes.dex" 426 | val outClassesDex = KFiles.joinDir(classesDexDir, classesDex) 427 | 428 | val dexSuccess = 429 | runDex(project, outClassesDex, dependencyManager.calculateDependencies(project, context), 430 | KFiles.joinDir(project.directory, project.classesDir(context))) 431 | 432 | val aaptSuccess = 433 | if (dexSuccess) { 434 | // 435 | // Add classes.dex to existing .ap_ 436 | // Because aapt doesn't handle directory moving, we need to cd to classes.dex's directory so 437 | // that classes.dex ends up in the root directory of the .ap_. 438 | // 439 | val aaptExitCode = AaptCommand(project, aapt(project), "add").apply { 440 | directory = File(outClassesDex).parentFile 441 | }.call(listOf("-v", KFiles.joinDir( 442 | File(AndroidFiles.temporaryApk(project, context.variant.shortArchiveName)).absolutePath), 443 | classesDex)) 444 | aaptExitCode == 0 445 | } else { 446 | false 447 | } 448 | 449 | return TaskResult(dexSuccess && aaptSuccess) 450 | } else { 451 | return TaskResult() 452 | } 453 | } 454 | 455 | private val DEFAULT_DEBUG_SIGNING_CONFIG = SigningConfig( 456 | SigningConfig.DEFAULT_STORE_FILE, 457 | SigningConfig.DEFAULT_STORE_PASSWORD, 458 | SigningConfig.DEFAULT_KEY_ALIAS, 459 | SigningConfig.DEFAULT_KEY_PASSWORD) 460 | 461 | /** 462 | * Sign the apk 463 | * Mac: 464 | * jarsigner -keystore ~/.android/debug.keystore -storepass android -keypass android -signedjar a.apk a.ap_ 465 | * androiddebugkey 466 | */ 467 | @Task(name = TASK_SIGN_APK, description = "Sign the apk file", runAfter = arrayOf(TASK_GENERATE_DEX), 468 | dependsOn = arrayOf("assemble")) 469 | fun taskSignApk(project: Project): TaskResult { 470 | val apk = AndroidFiles.apk(project, context.variant.shortArchiveName) 471 | val temporaryApk = AndroidFiles.temporaryApk(project, context.variant.shortArchiveName) 472 | val buildType = context.variant.buildType.name 473 | 474 | val config = configurationFor(project) 475 | var signingConfig = config!!.signingConfigs[buildType] 476 | 477 | if (signingConfig == null && buildType != "debug") { 478 | log(2, "Warning: No signingConfig found for product type \"$buildType\", using the \"debug\" signConfig") 479 | } 480 | 481 | signingConfig = DEFAULT_DEBUG_SIGNING_CONFIG 482 | 483 | val success = RunCommand("jarsigner").apply { 484 | // useInputStreamAsErrorIndicator = true 485 | }.run(listOf( 486 | "-keystore", signingConfig.storeFile, 487 | "-storepass", signingConfig.storePassword, 488 | "-keypass", signingConfig.keyPassword, 489 | "-signedjar", apk, 490 | temporaryApk, 491 | signingConfig.keyAlias 492 | )) 493 | log(1, "Created $apk") 494 | 495 | project.projectProperties.put(PROJECT_PROPERTY_APK_PATH, apk) 496 | 497 | return TaskResult(success == 0) 498 | } 499 | 500 | @Task(name = TASK_INSTALL, description = "Install the apk file", dependsOn = arrayOf(TASK_GENERATE_DEX, "assemble")) 501 | fun taskInstall(project: Project): TaskResult { 502 | 503 | /** 504 | * adb has weird ways of signaling errors, that's the best I've found so far. 505 | */ 506 | class AdbInstall : RunCommand(adb(project)) { 507 | override fun isSuccess(callSucceeded: Boolean, input: List, error: List) 508 | = input.filter { it.contains("Success")}.size > 0 509 | } 510 | 511 | val apk = AndroidFiles.apk(project, context.variant.shortArchiveName) 512 | val result = AdbInstall().useErrorStreamAsErrorIndicator(true).run( 513 | args = listOf("install", "-r", apk)) 514 | log(1, "Installed $apk") 515 | return TaskResult(result == 0) 516 | } 517 | 518 | private val classpathEntries = HashMultimap.create() 519 | 520 | // IClasspathContributor 521 | override fun classpathEntriesFor(project: Project?, context: KobaltContext): Collection { 522 | if (project == null || ! accept(project)) return emptyList() 523 | 524 | val aarFiles : Collection = classpathEntries.get(project.name) 525 | ?: emptyList() 526 | val classes : Collection 527 | = listOf(FileDependency(AndroidFiles.intermediatesClasses(project, context))) 528 | 529 | return aarFiles + classes 530 | } 531 | 532 | // IRepoContributor 533 | override fun reposFor(project: Project?): List { 534 | val config = configurationFor(project) 535 | 536 | return if (config != null) { 537 | val androidHome = androidHome(project!!) 538 | listOf(KFiles.joinDir(androidHome, "extras", "android", "m2repository"), 539 | (KFiles.joinDir(androidHome, "extras", "google", "m2repository"))) 540 | .map { HostConfig(Paths.get(it).toUri().toString()) } 541 | } else { 542 | emptyList() 543 | } 544 | } 545 | 546 | // IBuildDirectoryInterceptor 547 | override fun intercept(project: Project, context: KobaltContext, buildDirectory: String) 548 | = if (isAndroid(project)) { 549 | val c = AndroidFiles.intermediatesClasses(project, context) 550 | val result = Paths.get(project.directory).relativize(Paths.get(c)) 551 | result.toString() 552 | } else { 553 | buildDirectory 554 | } 555 | 556 | // ICompilerInterceptor 557 | /** 558 | * The output directory for the user's classes is kobaltBuild/intermediates/classes/classes (note: two "classes") 559 | * since that top directory contains additional directories for dex, pre-dexed, etc...) 560 | */ 561 | override fun intercept(project: Project, context: KobaltContext, actionInfo: CompilerActionInfo) 562 | : CompilerActionInfo { 563 | val result: CompilerActionInfo = 564 | if (isAndroid(project)) { 565 | val newBuildDir = project.classesDir(context) 566 | actionInfo.copy(outputDir = File(newBuildDir)) 567 | } else { 568 | actionInfo 569 | } 570 | return result 571 | } 572 | 573 | // IRunContributor 574 | override fun affinity(project: Project, context: KobaltContext): Int { 575 | val manifest = AndroidFiles.manifest(project) 576 | return if (File(manifest).exists()) IAffinity.DEFAULT_POSITIVE_AFFINITY else 0 577 | } 578 | 579 | override fun run(project: Project, context: KobaltContext, classpath: List): TaskResult { 580 | AndroidFiles.mergedManifest(project, context.variant).let { manifestPath -> 581 | FileInputStream(File(manifestPath)).use { ins -> 582 | // adb shell am start -n com.package.name/com.package.name.ActivityName 583 | val manifest = AndroidManifest(ins) 584 | RunCommand(adb(project)).useErrorStreamAsErrorIndicator(false).run(args = listOf( 585 | "shell", "am", "start", "-n", manifest.pkg + "/" + manifest.mainActivity)) 586 | return TaskResult() 587 | } 588 | } 589 | } 590 | 591 | private fun isAar(path: String) = path.contains("com.android.support") || path.contains("com.google.android") 592 | || path.contains("support-annotations") 593 | 594 | private fun isAar(id: MavenId) = (id.groupId == "com.android.support" || id.groupId == "com.google.android") 595 | && id.artifactId != "support-annotations" 596 | 597 | // IClasspathInterceptor 598 | /** 599 | * For each com.android.support dependency or aar packaging, add a classpath dependency that points to the 600 | * classes.jar inside that (exploded) aar. 601 | */ 602 | override fun intercept(project: Project, dependencies: List): List { 603 | val result = arrayListOf() 604 | dependencies.forEach { 605 | if (it.isMaven) { 606 | val mavenId = MavenId.create(it.id) 607 | if (isAar(mavenId) || mavenId.packaging == "aar") { 608 | val newDep = dependencyManager.createFile(AndroidFiles.explodedClassesJar(project, mavenId)) 609 | result.add(newDep) 610 | val id = MavenId.create(mavenId.groupId, mavenId.artifactId, "aar", null, it.version) 611 | result.add(dependencyManager.create(id.toId)) 612 | } else { 613 | result.add(it) 614 | } 615 | } else { 616 | result.add(it) 617 | } 618 | } 619 | return result 620 | } 621 | 622 | // IMavenIdInterceptor 623 | override fun intercept(mavenId: MavenId) : MavenId = 624 | if (isAar(mavenId)) { 625 | val version = mavenId.version ?: "" 626 | MavenId.createNoInterceptors("${mavenId.groupId}:${mavenId.artifactId}:aar:$version") 627 | } else { 628 | mavenId 629 | } 630 | 631 | /** 632 | * The extra source directories (without the project directory, which will be added by Kobalt). 633 | */ 634 | private val extraSourceDirectories = arrayListOf(File("src/main/aidl")) 635 | 636 | // ISourceDirectoryContributor 637 | override fun sourceDirectoriesFor(project: Project, context: KobaltContext): List = 638 | if (configurationFor(project) != null) extraSourceDirectories else emptyList() 639 | 640 | // IBuildConfigFieldContributor 641 | override fun fieldsFor(project: Project, context: KobaltContext): List { 642 | val result = arrayListOf() 643 | configurationFor(project)?.let { config -> 644 | result.add(BuildConfigField("String", "VERSION_NAME", "\"${config.defaultConfig.versionName}\"")) 645 | result.add(BuildConfigField("int", "VERSION_CODE", "${config.defaultConfig.versionCode}")) 646 | } 647 | return result 648 | } 649 | 650 | //ITaskContributor 651 | override fun tasksFor(project: Project, context: KobaltContext): List = taskContributor.dynamicTasks 652 | 653 | // ITemplateContributor 654 | override val templates = Templates().templates 655 | 656 | // IAssemblyContributor 657 | override fun assemble(project: Project, context: KobaltContext) : TaskResult { 658 | val pair = aars[project.name] 659 | if (pair != null) { 660 | aarGenerator.generate(project, context, pair.second) 661 | } 662 | return TaskResult() 663 | } 664 | 665 | val aars = hashMapOf>() 666 | 667 | fun addAar(project: Project, aarConfig: AarConfig) { 668 | aars[project.name] = Pair(project, aarConfig) 669 | } 670 | } 671 | 672 | class DefaultConfig(var minSdkVersion: Int? = 22, 673 | var maxSdkVersion: String? = null, 674 | var targetSdkVersion: String? = null, 675 | var versionCode: Int? = 1, 676 | var versionName: String? = null) { 677 | var buildConfig : BuildConfig? = BuildConfig() 678 | } 679 | 680 | @Directive 681 | fun Project.android(init: AndroidConfig.() -> Unit) : AndroidConfig = let { project -> 682 | return AndroidConfig(project).apply { 683 | init() 684 | (Kobalt.findPlugin(AndroidPlugin.PLUGIN_NAME) as AndroidPlugin).addConfiguration(project, this) 685 | } 686 | } 687 | 688 | class SigningConfig(var storeFile: String = SigningConfig.DEFAULT_STORE_FILE, 689 | var storePassword: String = SigningConfig.DEFAULT_STORE_PASSWORD, 690 | var keyAlias: String = SigningConfig.DEFAULT_KEY_ALIAS, 691 | var keyPassword: String = SigningConfig.DEFAULT_KEY_ALIAS) { 692 | 693 | companion object { 694 | val DEFAULT_STORE_FILE = homeDir(".android", "debug.keystore") 695 | val DEFAULT_STORE_PASSWORD = "android" 696 | val DEFAULT_KEY_ALIAS = "androiddebugkey" 697 | val DEFAULT_KEY_PASSWORD = "android" 698 | } 699 | } 700 | 701 | @Directive 702 | fun AndroidConfig.signingConfig(name: String, init: SigningConfig.() -> Unit) : SigningConfig = let { androidConfig -> 703 | SigningConfig().apply { 704 | init() 705 | androidConfig.addSigningConfig(name, project, this) 706 | } 707 | } 708 | 709 | class AarConfig{ 710 | var name: String? = null 711 | } 712 | 713 | /** 714 | * Create an aar file. 715 | */ 716 | @Directive 717 | fun AndroidConfig.aar(init: AarConfig.() -> Unit) { 718 | val aarConfig = AarConfig().apply { init() } 719 | (Kobalt.findPlugin(AndroidPlugin.PLUGIN_NAME) as AndroidPlugin).addAar(project, aarConfig) 720 | } 721 | 722 | //fun main(argv: Array) = com.beust.kobalt.main(argv) 723 | -------------------------------------------------------------------------------- /src/main/kotlin/com/beust/kobalt/plugin/android/AppInfo.kt: -------------------------------------------------------------------------------- 1 | package com.beust.kobalt.plugin.android 2 | 3 | import com.android.io.FileWrapper 4 | import com.android.xml.AndroidManifest 5 | import java.io.File 6 | 7 | /** 8 | * Manage the main application id for the app: values from androidConfig{} have precedence over values 9 | * found in the manifest. 10 | */ 11 | class AppInfo(val androidManifest: File, val config: AndroidConfig) { 12 | val abstractManifest = FileWrapper(androidManifest) 13 | 14 | val versionCode : Int 15 | get() = config.defaultConfig.versionCode ?: AndroidManifest.getVersionCode(abstractManifest) 16 | 17 | val versionName : String 18 | get() = config.defaultConfig.versionName ?: versionCode.toString() 19 | 20 | val minSdkVersion: Int 21 | get() = config.defaultConfig.minSdkVersion ?: (AndroidManifest.getMinSdkVersion(abstractManifest) as Int) 22 | 23 | val maxSdkVersion: Int? 24 | get() = config.defaultConfig.maxSdkVersion?.toInt() 25 | 26 | val targetSdkVersion: String? 27 | get() = config.defaultConfig.targetSdkVersion 28 | ?: AndroidManifest.getTargetSdkVersion(abstractManifest)?.toString() 29 | } 30 | -------------------------------------------------------------------------------- /src/main/kotlin/com/beust/kobalt/plugin/android/KobaltResourceMerger.kt: -------------------------------------------------------------------------------- 1 | package com.beust.kobalt.plugin.android 2 | 3 | import com.android.builder.core.AaptPackageProcessBuilder 4 | import com.android.builder.core.AndroidBuilder 5 | import com.android.builder.core.ErrorReporter 6 | import com.android.builder.core.LibraryRequest 7 | import com.android.builder.dependency.ManifestDependency 8 | import com.android.builder.dependency.SymbolFileProvider 9 | import com.android.builder.model.AaptOptions 10 | import com.android.builder.model.SyncIssue 11 | import com.android.builder.sdk.DefaultSdkLoader 12 | import com.android.builder.sdk.SdkLoader 13 | import com.android.ide.common.blame.Message 14 | import com.android.ide.common.internal.ExecutorSingleton 15 | import com.android.ide.common.process.* 16 | import com.android.ide.common.res2.* 17 | import com.android.manifmerger.ManifestMerger2 18 | import com.android.sdklib.AndroidTargetHash 19 | import com.android.sdklib.SdkManager 20 | import com.android.utils.ILogger 21 | import com.android.utils.StdLogger 22 | import com.beust.kobalt.Variant 23 | import com.beust.kobalt.api.IClasspathDependency 24 | import com.beust.kobalt.api.Project 25 | import com.beust.kobalt.maven.MavenId 26 | import com.beust.kobalt.misc.KFiles 27 | import com.beust.kobalt.misc.KobaltLogger 28 | import com.beust.kobalt.misc.log 29 | import com.beust.kobalt.misc.logWrap 30 | import java.io.File 31 | import java.nio.file.Paths 32 | import java.util.* 33 | 34 | class KobaltProcessResult : ProcessResult { 35 | override fun getExitValue(): Int { 36 | return 0 37 | } 38 | 39 | override fun assertNormalExitValue(): ProcessResult? { 40 | throw UnsupportedOperationException() 41 | } 42 | 43 | override fun rethrowFailure(): ProcessResult? { 44 | throw UnsupportedOperationException() 45 | } 46 | } 47 | 48 | class KobaltJavaProcessExecutor : JavaProcessExecutor { 49 | override fun execute(javaProcessInfo: JavaProcessInfo?, processOutputHandler: ProcessOutputHandler?) 50 | : ProcessResult? { 51 | log(1, "Executing " + javaProcessInfo!!) 52 | return KobaltProcessResult() 53 | } 54 | } 55 | 56 | class KobaltProcessOutputHandler : BaseProcessOutputHandler() { 57 | override fun handleOutput(processOutput: ProcessOutput) = 58 | log(3, "AndroidBuild output" + processOutput.standardOutput) 59 | } 60 | 61 | class KobaltErrorReporter : ErrorReporter(ErrorReporter.EvaluationMode.STANDARD) { 62 | override fun handleSyncError(data: String?, type: Int, msg: String?): SyncIssue? { 63 | throw UnsupportedOperationException() 64 | } 65 | 66 | override fun receiveMessage(message: Message?) { 67 | throw UnsupportedOperationException() 68 | } 69 | } 70 | 71 | class ProjectLayout { 72 | val mergeBlame: File? = null 73 | val publicText: File? = null 74 | 75 | } 76 | 77 | class KobaltResourceMerger(val androidHome: String) { 78 | fun run(project: Project, variant: Variant, config: AndroidConfig, aarDependencies: List) : List { 79 | val result = arrayListOf() 80 | val level = when(KobaltLogger.LOG_LEVEL) { 81 | 3 -> StdLogger.Level.VERBOSE 82 | 2 -> StdLogger.Level.WARNING 83 | else -> StdLogger.Level.ERROR 84 | } 85 | val logger = StdLogger(level) 86 | val androidBuilder = createAndroidBuilder(project, config, logger) 87 | 88 | log(2, "Merging resources") 89 | 90 | // 91 | // Assets 92 | // 93 | processAssets(project, variant, androidBuilder, aarDependencies) 94 | 95 | // 96 | // Manifests 97 | // 98 | val appInfo = processManifests(project, variant, androidBuilder, config) 99 | 100 | // 101 | // Resources 102 | // 103 | KobaltProcessOutputHandler().let { 104 | processResources(project, variant, androidBuilder, aarDependencies, logger, it, appInfo.minSdkVersion) 105 | result.addAll(mergeResources(project, variant, androidBuilder, aarDependencies, it)) 106 | } 107 | return result 108 | } 109 | 110 | private fun createAndroidBuilder(project: Project, config: AndroidConfig, logger: ILogger): AndroidBuilder { 111 | val processExecutor = DefaultProcessExecutor(logger) 112 | val javaProcessExecutor = KobaltJavaProcessExecutor() 113 | val sdkLoader : SdkLoader = DefaultSdkLoader.getLoader(File(androidHome)) 114 | val result = AndroidBuilder(project.name, "kobalt-android-plugin", 115 | processExecutor, 116 | javaProcessExecutor, 117 | KobaltErrorReporter(), 118 | logger, 119 | false /* verbose */) 120 | 121 | val libraryRequests = arrayListOf() 122 | val sdk = sdkLoader.getSdkInfo(logger) 123 | val sdkManager = SdkManager.createManager(androidHome, logger) 124 | val maxPlatformTarget = sdkManager.targets.filter { it.isPlatform }.last() 125 | val maxPlatformTargetHash = AndroidTargetHash.getPlatformHashString(maxPlatformTarget.version) 126 | 127 | result.setTargetInfo(sdk, 128 | sdkLoader.getTargetInfo(maxPlatformTargetHash, maxPlatformTarget.buildToolInfo.revision, logger), 129 | libraryRequests) 130 | return result 131 | } 132 | 133 | /** 134 | * Create a ManifestDependency suitable for the Android builder based on a Maven Dependency. 135 | */ 136 | private fun create(project: Project, md: IClasspathDependency, shortIds: HashMap) = 137 | object: ManifestDependency { 138 | override fun getManifest() = File(AndroidFiles.explodedManifest(project, MavenId.create(md.id))) 139 | 140 | override fun getName() = md.jarFile.get().path 141 | 142 | override fun getManifestDependencies(): List 143 | = createLibraryDependencies(project, md.directDependencies(), shortIds) 144 | } 145 | 146 | private fun createLibraryDependencies(project: Project, dependencies: List, 147 | shortIds : HashMap) 148 | : List { 149 | val result = arrayListOf() 150 | dependencies.filter { 151 | it.jarFile.get().path.endsWith(".aar") 152 | }.forEach { dep -> 153 | val manifestDependency = shortIds.computeIfAbsent(dep.shortId, { create(project, dep, shortIds) }) 154 | result.add(manifestDependency) 155 | } 156 | return result 157 | } 158 | 159 | private fun processAssets(project: Project, variant: Variant, androidBuilder: AndroidBuilder, 160 | aarDependencies: List) { 161 | logWrap(2, " Processing assets...", "done") { 162 | val intermediates = File( 163 | KFiles.joinDir(AndroidFiles.intermediates(project), "assets", variant.toIntermediateDir())) 164 | aarDependencies.forEach { 165 | val assetDir = File(it, "assets") 166 | if (assetDir.exists()) { 167 | KFiles.copyRecursively(assetDir, intermediates) 168 | } 169 | } 170 | } 171 | } 172 | 173 | private fun processManifests(project: Project, variant: Variant, androidBuilder: AndroidBuilder, 174 | config: AndroidConfig): AppInfo { 175 | val mainManifest = File(project.directory, "src/main/AndroidManifest.xml") 176 | val appInfo = AppInfo(mainManifest, config) 177 | logWrap(2, " Processing manifests...", "done") { 178 | val manifestOverlays = variant.allDirectories(project).map { 179 | File(project.directory, "src/$it/AndroidManifest.xml") 180 | }.filter { 181 | it.exists() 182 | } 183 | val shortIds = hashMapOf() 184 | val libraries = createLibraryDependencies(project, project.compileDependencies, shortIds) 185 | val outManifest = AndroidFiles.mergedManifest(project, variant) 186 | val outAaptSafeManifestLocation = KFiles.joinDir(project.directory, project.buildDirectory, "generatedSafeAapt") 187 | val reportFile = File(KFiles.joinDir(project.directory, project.buildDirectory, "manifest-merger-report.txt")) 188 | androidBuilder.mergeManifests(mainManifest, manifestOverlays, libraries, 189 | null /* package override */, 190 | appInfo.versionCode, 191 | appInfo.versionName, 192 | appInfo.minSdkVersion.toString(), 193 | appInfo.targetSdkVersion, 194 | appInfo.maxSdkVersion, 195 | outManifest, 196 | outAaptSafeManifestLocation, 197 | // TODO: support aar too 198 | ManifestMerger2.MergeType.APPLICATION, 199 | // ManifestMerger2.MergeType.LIBRARY, 200 | emptyMap() /* placeHolders */, 201 | reportFile) 202 | } 203 | return appInfo 204 | } 205 | 206 | private fun processResources(project: Project, variant: Variant, androidBuilder: AndroidBuilder, 207 | aarDependencies: List, logger: ILogger, processOutputHandler: KobaltProcessOutputHandler, 208 | minSdk: Int) { 209 | logWrap(2, " Processing resources...", "done") { 210 | val layout = ProjectLayout() 211 | val preprocessor = NoOpResourcePreprocessor() 212 | val outputDir = AndroidFiles.mergedResources(project, variant) 213 | val resourceMerger = ResourceMerger(minSdk) 214 | val fullVariantDir = File(variant.toCamelcaseDir()) 215 | val srcList = setOf("main", variant.productFlavor.name, variant.buildType.name, fullVariantDir.path) 216 | .map { project.directory + File.separator + "src" + File.separator + it } 217 | 218 | // TODO: figure out why the badSrcList is bad. All this information should be coming from the Variant 219 | // Figured it out: using hardcoded "resources" instead of "res" 220 | val badSrcList = variant.resourceDirectories(project).map { it.path } 221 | val goodAarList = aarDependencies.map { it.path + File.separator } 222 | (goodAarList + srcList).map { it + File.separator + "res" }.forEach { path -> 223 | with(ResourceSet(path)) { 224 | addSource(File(path)) 225 | loadFromFiles(logger) 226 | setGeneratedSet(GeneratedResourceSet(this)) 227 | resourceMerger.addDataSet(this) 228 | } 229 | } 230 | 231 | val writer = MergedResourceWriter(File(outputDir), 232 | androidBuilder.getAaptCruncher(processOutputHandler), 233 | false /* don't crunch */, 234 | false /* don't process 9patch */, 235 | layout.publicText, 236 | layout.mergeBlame, 237 | preprocessor) 238 | resourceMerger.mergeData(writer, true) 239 | } 240 | } 241 | 242 | fun shutdown() { 243 | ExecutorSingleton.getExecutor().shutdown() 244 | } 245 | 246 | /** 247 | * @return the extra source directories 248 | */ 249 | private fun mergeResources(project: Project, variant: Variant, androidBuilder: AndroidBuilder, 250 | aarDependencies: List, processOutputHandler: KobaltProcessOutputHandler) : List { 251 | val result = arrayListOf() 252 | logWrap(2, " Merging resources...", "done") { 253 | 254 | val aaptOptions = object : AaptOptions { 255 | override fun getAdditionalParameters() = emptyList() 256 | override fun getFailOnMissingConfigEntry() = false 257 | override fun getIgnoreAssets() = null 258 | override fun getNoCompress() = null 259 | } 260 | 261 | val aaptCommand = AaptPackageProcessBuilder(File(AndroidFiles.mergedManifest(project, variant)), 262 | aaptOptions) 263 | 264 | fun toSymbolFileProvider(aarDirectory: File) = object : SymbolFileProvider { 265 | override fun getManifest() = File(aarDirectory, "AndroidManifest.xml") 266 | override fun isOptional() = false 267 | override fun getSymbolFile() = File(aarDirectory, "R.txt") 268 | } 269 | 270 | val variantDir = variant.toIntermediateDir() 271 | val generated = KFiles.joinAndMakeDir(project.directory, project.buildDirectory, "symbols") 272 | 273 | val rDirectory = KFiles.joinAndMakeDir(KFiles.generatedSourceDir(project, variant, "r")) 274 | result.add(Paths.get(project.directory).relativize(Paths.get(rDirectory)).toString()) 275 | 276 | with(aaptCommand) { 277 | sourceOutputDir = rDirectory 278 | val libraries = aarDependencies.map { toSymbolFileProvider(it) } 279 | // TODO: setType for libraries 280 | setLibraries(libraries) 281 | setResFolder(File(AndroidFiles.mergedResources(project, variant))) 282 | setAssetsFolder(File(KFiles.joinAndMakeDir(AndroidFiles.intermediates(project), "assets", variantDir))) 283 | setResPackageOutput(AndroidFiles.temporaryApk(project, variant.shortArchiveName)) 284 | symbolOutputDir = generated 285 | 286 | // aaptCommand.setSourceOutputDir(generated) 287 | // aaptCommand.setPackageForR(pkg) 288 | // aaptCommand.setProguardOutput(proguardTxt) 289 | // aaptCommand.setType(if (lib) VariantType.LIBRARY else VariantType.DEFAULT) 290 | // setDebuggable(true) 291 | // setVerbose() 292 | } 293 | 294 | androidBuilder.processResources(aaptCommand, true, processOutputHandler) 295 | } 296 | return result 297 | } 298 | 299 | fun dex(project: Project) { 300 | // androidBuilder.createMainDexList() 301 | } 302 | } 303 | -------------------------------------------------------------------------------- /src/main/kotlin/com/beust/kobalt/plugin/android/Proguard.kt: -------------------------------------------------------------------------------- 1 | package com.beust.kobalt.plugin.android 2 | 3 | import com.beust.kobalt.misc.KFiles 4 | 5 | class Proguard(val androidHome: String) { 6 | val proguardHome = KFiles.joinDir(androidHome, "tools", "proguard") 7 | val proguardCommand = KFiles.joinDir(proguardHome, "bin", "proguard.sh") 8 | 9 | fun getDefaultProguardFile(name: String) = KFiles.joinDir(proguardHome, name) 10 | } 11 | -------------------------------------------------------------------------------- /src/main/kotlin/com/beust/kobalt/plugin/android/RunCommand.kt: -------------------------------------------------------------------------------- 1 | package com.beust.kobalt.plugin.android 2 | 3 | import com.beust.kobalt.misc.log 4 | import java.io.BufferedReader 5 | import java.io.File 6 | import java.io.InputStream 7 | import java.io.InputStreamReader 8 | import java.util.concurrent.TimeUnit 9 | 10 | class RunCommandInfo { 11 | lateinit var command: String 12 | var args : List = arrayListOf() 13 | var directory : File = File("") 14 | var env : Map = hashMapOf() 15 | 16 | /** 17 | * Some commands fail but return 0, so the only way to find out if they failed is to look 18 | * at the error stream. However, some commands succeed but output text on the error stream. 19 | * This field is used to specify how errors are caught. 20 | */ 21 | var useErrorStreamAsErrorIndicator : Boolean = true 22 | var useInputStreamAsErrorIndicator : Boolean = false 23 | 24 | var errorCallback: Function1, Unit> = NewRunCommand.DEFAULT_ERROR 25 | var successCallback: Function1, Unit> = NewRunCommand.DEFAULT_SUCCESS 26 | 27 | var isSuccess: (Boolean, List, List) -> Boolean = { 28 | isSuccess: Boolean, 29 | input: List, 30 | error: List -> 31 | var hasErrors = ! isSuccess 32 | if (useErrorStreamAsErrorIndicator && ! hasErrors) { 33 | hasErrors = hasErrors || error.size > 0 34 | } 35 | if (useInputStreamAsErrorIndicator && ! hasErrors) { 36 | hasErrors = hasErrors || input.size > 0 37 | } 38 | 39 | ! hasErrors 40 | } 41 | } 42 | 43 | fun runCommand(init: RunCommandInfo.() -> Unit) = NewRunCommand(RunCommandInfo().apply { init() }).invoke() 44 | 45 | open class NewRunCommand(val info: RunCommandInfo) { 46 | 47 | companion object { 48 | val DEFAULT_SUCCESS = { output: List -> } 49 | // val DEFAULT_SUCCESS_VERBOSE = { output: List -> log(2, "Success:\n " + output.joinToString("\n"))} 50 | // val defaultSuccess = DEFAULT_SUCCESS 51 | val DEFAULT_ERROR = { 52 | output: List -> 53 | error(output.joinToString("\n ")) 54 | } 55 | } 56 | 57 | // fun useErrorStreamAsErrorIndicator(f: Boolean) : RunCommand { 58 | // useErrorStreamAsErrorIndicator = f 59 | // return this 60 | // } 61 | 62 | fun invoke() : Int { 63 | val allArgs = arrayListOf() 64 | allArgs.add(info.command) 65 | allArgs.addAll(info.args) 66 | 67 | val pb = ProcessBuilder(allArgs) 68 | pb.directory(info.directory) 69 | log(2, "Running command in directory ${info.directory.absolutePath}" + 70 | "\n " + allArgs.joinToString(" ").replace("\\", "/")) 71 | pb.environment().let { pbEnv -> 72 | info.env.forEach { 73 | pbEnv.put(it.key, it.value) 74 | } 75 | } 76 | 77 | val process = pb.start() 78 | 79 | // Run the command and collect the return code and streams 80 | val returnCode = process.waitFor(30, TimeUnit.SECONDS) 81 | val input = if (process.inputStream.available() > 0) fromStream(process.inputStream) 82 | else listOf() 83 | val error = if (process.errorStream.available() > 0) fromStream(process.errorStream) 84 | else listOf() 85 | 86 | // Check to see if the command succeeded 87 | val isSuccess = isSuccess(returnCode, input, error) 88 | 89 | if (isSuccess) { 90 | info.successCallback(input) 91 | } else { 92 | info.errorCallback(error + input) 93 | } 94 | 95 | return if (isSuccess) 0 else 1 96 | } 97 | 98 | /** 99 | * Subclasses can override this method to do their own error handling, since commands can 100 | * have various ways to signal errors. 101 | */ 102 | open protected fun isSuccess(isSuccess: Boolean, input: List, error: List) : Boolean { 103 | var hasErrors = ! isSuccess 104 | if (info.useErrorStreamAsErrorIndicator && ! hasErrors) { 105 | hasErrors = hasErrors || error.size > 0 106 | } 107 | if (info.useInputStreamAsErrorIndicator && ! hasErrors) { 108 | hasErrors = hasErrors || input.size > 0 109 | } 110 | 111 | return ! hasErrors 112 | } 113 | 114 | /** 115 | * Turn the given InputStream into a list of strings. 116 | */ 117 | private fun fromStream(ins: InputStream) : List { 118 | val result = arrayListOf() 119 | val br = BufferedReader(InputStreamReader(ins)) 120 | var line = br.readLine() 121 | 122 | while (line != null) { 123 | result.add(line) 124 | line = br.readLine() 125 | } 126 | return result 127 | } 128 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/beust/kobalt/plugin/android/SdkDownloader.kt: -------------------------------------------------------------------------------- 1 | package com.beust.kobalt.plugin.android 2 | 3 | import com.android.SdkConstants 4 | import com.beust.kobalt.KobaltException 5 | import com.beust.kobalt.api.Project 6 | import com.beust.kobalt.homeDir 7 | import com.beust.kobalt.misc.KFiles 8 | import com.beust.kobalt.misc.log 9 | import com.beust.kobalt.misc.warn 10 | import org.rauschig.jarchivelib.ArchiveFormat 11 | import org.rauschig.jarchivelib.ArchiverFactory 12 | import org.rauschig.jarchivelib.CompressionType 13 | import java.io.File 14 | import java.io.InputStreamReader 15 | import java.io.OutputStreamWriter 16 | import java.net.URL 17 | import java.nio.file.Files 18 | 19 | /** 20 | * Automatically download the Android SDK if it can't be found and then any other necessary components. 21 | * If the Android Home is not passed in parameter, look it up in $ANDROID_HOME and if that variable 22 | * isn't defined, download it in ~/.android-sdk/. Adapted from Jake Wharton's android-sdk-manager 23 | * plug-in. 24 | * 25 | * @author Cedric Beust 26 | */ 27 | class SdkUpdater(val configAndroidHome: String?, val compileSdkVersion: String?, val buildToolsVersion: String?, 28 | val dryMode: Boolean = false) { 29 | val FD_BUILD_TOOLS = com.android.SdkConstants.FD_BUILD_TOOLS 30 | val FD_PLATFORM = com.android.SdkConstants.FD_PLATFORMS 31 | val FD_PLATFORM_TOOLS = com.android.SdkConstants.FD_PLATFORM_TOOLS 32 | 33 | val androidHome: String by lazy { 34 | maybeInstallAndroid() 35 | } 36 | 37 | enum class SdkDownload(val platform: String, val extension: String) { 38 | WINDOWS("windows", "zip"), 39 | LINUX("linux", "tgz"), 40 | DARWIN("macosx", "zip") 41 | } 42 | 43 | companion object { 44 | private fun log(s: String) = log(1, s) 45 | private fun logVerbose(s: String) = log(2, " $s") 46 | private fun logVeryVerbose(s: String) = log(3, " s") 47 | } 48 | 49 | fun maybeInstall(project: Project): String { 50 | logVerbose("Android home is $androidHome") 51 | 52 | // Build tools 53 | // android update sdk --all --filter build-tools-21.1.0 --no-ui 54 | if (buildToolsVersion != null) { 55 | maybeInstall(FD_BUILD_TOOLS + "-" + buildToolsVersion, 56 | listOf(FD_BUILD_TOOLS, buildToolsVersion)) 57 | } 58 | 59 | // Platform tools 60 | // android update sdk --all --filter platform-tools --no-ui 61 | maybeInstall(FD_PLATFORM_TOOLS, listOf(FD_PLATFORM_TOOLS)) 62 | 63 | // Compilation version 64 | // android update sdk --all --filter android-22 --no-ui 65 | if (compileSdkVersion != null) { 66 | maybeInstall("android-$compileSdkVersion", listOf(FD_PLATFORM, "android-$compileSdkVersion")) 67 | } 68 | 69 | // Android Support libraries 70 | // android update sdk --all --filter extra-android-m2repository --no-ui 71 | maybeInstallRepository(project, "com.android.support", "extra-android-m2repository", 72 | hasAndroidM2Repository(androidHome)) 73 | 74 | // Google Support libraries 75 | // android update sdk --all --filter extra-google-m2repository --no-ui 76 | maybeInstallRepository(project, "com.google.android", "extra-google-m2repository", 77 | hasGoogleM2Repository(androidHome)) 78 | 79 | return androidHome 80 | } 81 | 82 | private fun maybeInstallRepository(project: Project, id: String, filter: String, directoryExists: Boolean) { 83 | if (! directoryExists && project.compileDependencies.any { it.id.contains(id) }) { 84 | log("Downloading Maven repository for $filter") 85 | update(filter) 86 | } else { 87 | logVerbose("Found Maven repository for $filter") 88 | } 89 | } 90 | 91 | private fun hasAndroidM2Repository(androidHome: String) 92 | = File(KFiles.joinDir(androidHome, "extras", "android", "m2repository")).exists() 93 | 94 | private fun hasGoogleM2Repository(androidHome: String) 95 | = File(KFiles.joinDir(androidHome, "extras", "google", "m2repository")).exists() 96 | 97 | private val sdk: SdkDownload 98 | get() { 99 | val osName = System.getProperty("os.name").toLowerCase() 100 | return if (osName.contains("windows")) SdkDownload.WINDOWS 101 | else if (osName.contains("mac os x") || osName.contains("darwin") 102 | || osName.contains("osx")) SdkDownload.DARWIN 103 | else SdkDownload.LINUX 104 | } 105 | 106 | private fun androidZipName(sdkVersion: String, suffix: String, ext: String) 107 | = "android-sdk_r$sdkVersion-$suffix.$ext" 108 | 109 | private fun downloadUrl(sdkVersion: String, suffix: String, ext: String) 110 | = "http://dl.google.com/android/android-sdk_r$sdkVersion-$suffix.$ext" 111 | 112 | private val SDK_LATEST_VERSION = "24.4.1" 113 | private val androidDownloadDirectory = homeDir(".android-sdk") 114 | private fun newAndroidHome(platform: String) = 115 | KFiles.makeDir(androidDownloadDirectory, "android-sdk-$platform").absolutePath 116 | 117 | private fun maybeInstallAndroid(): String { 118 | val envHome = System.getenv("ANDROID_HOME") 119 | fun validAndroidHome(home: String?) = home != null && File(androidCommand(home)).exists() 120 | val androidHome = 121 | if (envHome != null) { 122 | if (! validAndroidHome(envHome)) { 123 | throw KobaltException("Invalid \$ANDROID_HOME $envHome, please specify a valid one or none at all") 124 | } else { 125 | envHome 126 | } 127 | } else { 128 | configAndroidHome ?: newAndroidHome(sdk.platform) 129 | } 130 | 131 | val androidHomeDir = File(androidHome) 132 | 133 | // Download 134 | val androidHomeParent = File(androidHome).parentFile 135 | val zipFile = File(androidDownloadDirectory, androidZipName(SDK_LATEST_VERSION, sdk.platform, sdk.extension)) 136 | val androidCommand = File(androidCommand(androidHomeDir.absolutePath)) 137 | 138 | if (! androidHomeDir.exists() || ! androidCommand.exists()) { 139 | val downloadUrl = downloadUrl(SDK_LATEST_VERSION, sdk.platform, sdk.extension) 140 | if (!dryMode) { 141 | log("Android SDK not found at $androidHome, downloading it") 142 | if (! zipFile.exists()) { 143 | downloadFile(downloadUrl, zipFile.absolutePath) 144 | } else { 145 | log("Found an existing distribution, not downloading it again") 146 | } 147 | 148 | val archiver = if (sdk.extension == "zip") ArchiverFactory.createArchiver(ArchiveFormat.ZIP) 149 | else ArchiverFactory.createArchiver(ArchiveFormat.TAR, CompressionType.GZIP) 150 | archiver.extract(zipFile, androidHomeParent) 151 | File(androidCommand(androidHomeDir.absolutePath)).setExecutable(true) 152 | } else { 153 | logVerbose("dryMode is enabled, not downloading $downloadUrl") 154 | } 155 | } 156 | 157 | return androidHome 158 | } 159 | 160 | /** 161 | * Download the given url to a file. 162 | */ 163 | private fun downloadFile(url: String, outFile: String): File { 164 | val buffer = ByteArray(1000000) 165 | val hasTerminal = System.console() != null 166 | log("Downloading " + url) 167 | val from = URL(url).openConnection().inputStream 168 | val tmpFile = Files.createTempFile("kobalt-android-sdk", "").toFile() 169 | tmpFile.outputStream().use { to -> 170 | var bytesRead = from.read(buffer) 171 | var bytesSoFar = 0L 172 | while (bytesRead != -1) { 173 | to.write(buffer, 0, bytesRead) 174 | bytesSoFar += bytesRead.toLong() 175 | bytesRead = from.read(buffer) 176 | } 177 | } 178 | 179 | val toFile = File(outFile).apply { delete() } 180 | tmpFile.renameTo(File(outFile)) 181 | logVerbose("Downloaded the Android SDK to $toFile") 182 | return toFile 183 | } 184 | 185 | private fun maybeInstall(filter: String, dirList: List) { 186 | val dir = KFiles.joinDir(androidHome, *dirList.toTypedArray()) 187 | if (!File(dir).exists()) { 188 | log("Downloading $dir") 189 | update(filter) 190 | } else { 191 | logVerbose("Found $dir") 192 | } 193 | } 194 | 195 | private fun androidCommand(androidHome: String) = KFiles.joinDir(androidHome, "tools", 196 | SdkConstants.androidCmdName()) 197 | 198 | /** 199 | * Launch the "android" command with the given filter. 200 | */ 201 | private fun update(filter: String) : Int { 202 | val fullCommandArgs = listOf(androidCommand(androidHome), "update", "sdk", "--all", "--filter", filter, 203 | "--no-ui") + 204 | (if (dryMode) listOf("-n") else emptyList()) 205 | val fullCommand = fullCommandArgs.joinToString(" ") 206 | logVerbose("Launching " + fullCommand) 207 | val process = ProcessBuilder(fullCommandArgs) 208 | .redirectErrorStream(true) 209 | .start() 210 | 211 | // Press 'y' and then enter on the license prompt. 212 | OutputStreamWriter(process.outputStream).use { 213 | it.write("y\n") 214 | } 215 | 216 | // Pipe the command output to our log. 217 | InputStreamReader(process.inputStream).useLines { seq -> 218 | seq.forEach { 219 | logVeryVerbose(it) 220 | } 221 | } 222 | 223 | // Save the error stream in case something goes wrong 224 | val errors = arrayListOf() 225 | InputStreamReader(process.errorStream).useLines { seq -> 226 | seq.forEach { 227 | errors.add(it) 228 | } 229 | } 230 | 231 | val result = process.waitFor() 232 | if (result != 0) { 233 | warn("$fullCommand didn't complete successfully: $result") 234 | errors.forEach { warn(" $it") } 235 | } else { 236 | logVerbose("$fullCommand completed successfully") 237 | } 238 | return result 239 | } 240 | } 241 | 242 | fun main(argv: Array) { 243 | val extension = "zip" 244 | val archiver = if (extension == "zip") ArchiverFactory.createArchiver(ArchiveFormat.ZIP) 245 | else ArchiverFactory.createArchiver(ArchiveFormat.TAR, CompressionType.GZIP) 246 | // archiver.extract(zipFile, File(androidBaseDir)) 247 | archiver.extract(File(homeDir("t/android-macosx.zip")), File(homeDir("t/abcDir"))) 248 | 249 | // SdkDownload.downloader.download() 250 | // SdkUpdater(null, "22", "21.1.0").maybeInstall() 251 | } 252 | -------------------------------------------------------------------------------- /src/main/kotlin/com/beust/kobalt/plugin/android/Templates.kt: -------------------------------------------------------------------------------- 1 | package com.beust.kobalt.plugin.android 2 | 3 | import com.beust.kobalt.api.ITemplateContributor 4 | import com.beust.kobalt.api.ResourceJarTemplate 5 | 6 | /** 7 | * Run the Android template. 8 | */ 9 | class Templates : ITemplateContributor { 10 | class TemplateInfo(val name: String, val description: String, val jarFileName: String) 11 | 12 | override val templates = listOf( 13 | Template(TemplateInfo("androidJava", 14 | "Generate a simple Android Java project", 15 | "templates/androidJavaTemplate.jar")), 16 | Template(TemplateInfo("androidKotlin", 17 | "Generate a simple Android Kotlin project", 18 | "templates/androidKotlinTemplate.jar") 19 | )) 20 | 21 | class Template(info: TemplateInfo) : ResourceJarTemplate(info.jarFileName, Templates::class.java.classLoader) { 22 | override val templateName = info.name 23 | override val templateDescription = info.description 24 | override val pluginName = AndroidPlugin.PLUGIN_NAME 25 | } 26 | } 27 | 28 | //fun main(argv: Array) { 29 | // val jarFile = homeDir("kotlin", "kobalt-android", "src", "main", "resources", 30 | // "android-java-archetype.jar") 31 | // Archetypes.Template.extractFile(JarInputStream(FileInputStream(jarFile)), File(homeDir("t/arch2"))) 32 | //} 33 | -------------------------------------------------------------------------------- /src/main/resources/META-INF/kobalt-plugin.xml: -------------------------------------------------------------------------------- 1 | 2 | kobalt-android 3 | 4 | com.beust.kobalt.plugin.android.AndroidPlugin 5 | 6 | -------------------------------------------------------------------------------- /src/main/resources/templates/androidJavaTemplate.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cbeust/kobalt-android/e69a25c6146e544de81bfc85ceacff238d18e953/src/main/resources/templates/androidJavaTemplate.jar -------------------------------------------------------------------------------- /src/main/resources/templates/androidKotlinTemplate.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cbeust/kobalt-android/e69a25c6146e544de81bfc85ceacff238d18e953/src/main/resources/templates/androidKotlinTemplate.jar --------------------------------------------------------------------------------