Fix noCompress support in aapt.
[android/platform/tools/build.git] / gradle / src / main / groovy / com / android / build / gradle / AndroidPlugin.groovy
1 /*
2  * Copyright (C) 2012 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.android.build.gradle
17
18 import com.android.build.gradle.internal.AndroidSourceSet
19 import com.android.build.gradle.internal.ApplicationVariant
20 import com.android.build.gradle.internal.BuildTypeData
21 import com.android.build.gradle.internal.ProductFlavorData
22 import com.android.build.gradle.internal.ProductionAppVariant
23 import com.android.build.gradle.internal.TestAppVariant
24 import com.android.builder.BuildType
25 import com.android.builder.ProductFlavor
26 import com.android.builder.VariantConfiguration
27 import org.gradle.api.Plugin
28 import org.gradle.api.Project
29 import org.gradle.api.Task
30 import org.gradle.api.tasks.compile.Compile
31 import org.gradle.internal.reflect.Instantiator
32
33 class AndroidPlugin extends AndroidBasePlugin implements Plugin<Project> {
34     private final Map<String, BuildTypeData> buildTypes = [:]
35     private final Map<String, ProductFlavorData> productFlavors = [:]
36
37     private AndroidExtension extension
38
39     private Task installAllForTests
40     private Task runAllTests
41     private Task uninstallAll
42     private Task assembleTest
43
44     @Override
45     void apply(Project project) {
46         super.apply(project)
47
48         def buildTypes = project.container(BuildType)
49         // TODO - do the decoration by default
50         def productFlavors = project.container(ProductFlavor) { name ->
51             project.services.get(Instantiator).newInstance(ProductFlavor, name)
52         }
53
54         extension = project.extensions.create('android', AndroidExtension,
55                 buildTypes, productFlavors)
56         setDefaultConfig(extension.defaultConfig)
57
58         findSdk(project)
59
60         buildTypes.whenObjectAdded { BuildType buildType ->
61             addBuildType(buildType)
62         }
63         buildTypes.whenObjectRemoved {
64             throw new UnsupportedOperationException("Removing build types is not implemented yet.")
65         }
66
67         buildTypes.create(BuildType.DEBUG)
68         buildTypes.create(BuildType.RELEASE)
69
70         productFlavors.whenObjectAdded { ProductFlavor productFlavor ->
71             addProductFlavor(productFlavor)
72         }
73
74         productFlavors.whenObjectRemoved {
75             throw new UnsupportedOperationException(
76                     "Removing product flavors is not implemented yet.")
77         }
78
79         installAllForTests = project.tasks.add("installAllForTests")
80         installAllForTests.group = "Verification"
81         installAllForTests.description = "Installs all applications needed to run tests."
82
83         runAllTests = project.tasks.add("runAllTests")
84         runAllTests.group = "Verification"
85         runAllTests.description = "Runs all tests."
86
87         uninstallAll = project.tasks.add("uninstallAll")
88         uninstallAll.description = "Uninstall all applications."
89         uninstallAll.group = "Install"
90
91         project.tasks.check.dependsOn installAllForTests, runAllTests
92
93         project.afterEvaluate {
94             createAndroidTasks()
95         }
96     }
97
98     private void addBuildType(BuildType buildType) {
99         if (buildType.name.startsWith("test")) {
100             throw new RuntimeException("BuildType names cannot start with 'test'")
101         }
102         if (productFlavors.containsKey(buildType.name)) {
103             throw new RuntimeException("BuildType names cannot collide with ProductFlavor names")
104         }
105
106         def sourceSet = project.sourceSets.add(buildType.name)
107
108         BuildTypeData buildTypeData = new BuildTypeData(buildType, sourceSet, project)
109         project.tasks.assemble.dependsOn buildTypeData.assembleTask
110
111         buildTypes[buildType.name] = buildTypeData
112     }
113
114     private void addProductFlavor(ProductFlavor productFlavor) {
115         if (productFlavor.name.startsWith("test")) {
116             throw new RuntimeException("ProductFlavor names cannot start with 'test'")
117         }
118         if (productFlavors.containsKey(productFlavor.name)) {
119             throw new RuntimeException("ProductFlavor names cannot collide with BuildType names")
120         }
121
122         def mainSourceSet = project.sourceSets.add(productFlavor.name)
123         def testSourceSet = project.sourceSets.add("test${productFlavor.name.capitalize()}")
124
125         ProductFlavorData productFlavorData = new ProductFlavorData(
126                 productFlavor, mainSourceSet, testSourceSet, project)
127         project.tasks.assemble.dependsOn productFlavorData.assembleTask
128
129         productFlavors[productFlavor.name] = productFlavorData
130     }
131
132     private void createAndroidTasks() {
133         if (productFlavors.isEmpty()) {
134             createTasksForDefaultBuild()
135         } else {
136             // there'll be more than one test app, so we need a top level assembleTest
137             assembleTest = project.tasks.add("assembleTest")
138             assembleTest.group = "Build"
139             assembleTest.description = "Assembles all the Test applications"
140
141             productFlavors.values().each { ProductFlavorData productFlavorData ->
142                 createTasksForFlavoredBuild(productFlavorData)
143             }
144         }
145     }
146
147     /**
148      * Creates Tasks for non-flavored build. This means assembleDebug, assembleRelease, and other
149      * assemble<Type> are directly building the <type> build instead of all build of the given
150      * <type>.
151      */
152     private createTasksForDefaultBuild() {
153
154         BuildTypeData testData = buildTypes[extension.testBuildType]
155         if (testData == null) {
156             throw new RuntimeException("Test Build Type '$extension.testBuildType' does not exist.")
157         }
158
159         VariantConfiguration testedVariantConfig = null
160         ProductionAppVariant testedVariant = null
161
162         ProductFlavorData defaultConfigData = getDefaultConfigData();
163
164         for (BuildTypeData buildTypeData : buildTypes.values()) {
165
166             def variantConfig = new VariantConfiguration(
167                     defaultConfigData.productFlavor, defaultConfigData.androidSourceSet,
168                     buildTypeData.buildType, buildTypeData.androidSourceSet,
169                     VariantConfiguration.Type.DEFAULT)
170
171             boolean isTestedVariant = (buildTypeData == testData)
172
173             ProductionAppVariant productionAppVariant = addVariant(variantConfig,
174                     buildTypeData.assembleTask, isTestedVariant)
175
176             if (isTestedVariant) {
177                 testedVariantConfig = variantConfig
178                 testedVariant = productionAppVariant
179             }
180         }
181
182         assert testedVariantConfig != null && testedVariant != null
183
184         def variantTestConfig = new VariantConfiguration(
185                 defaultConfigData.productFlavor, defaultConfigData.androidTestSourceSet,
186                 testData.buildType, null,
187                 VariantConfiguration.Type.TEST)
188
189         addTestVariant(variantTestConfig, testedVariantConfig, testedVariant)
190     }
191
192     /**
193      * Creates Task for a given flavor. This will create tasks for all build types for the given
194      * flavor.
195      * @param productFlavorData the flavor to build.
196      */
197     private createTasksForFlavoredBuild(ProductFlavorData productFlavorData) {
198
199         BuildTypeData testData = buildTypes[extension.testBuildType]
200         if (testData == null) {
201             throw new RuntimeException("Test Build Type '$extension.testBuildType' does not exist.")
202         }
203
204         VariantConfiguration testedVariantConfig = null
205         ProductionAppVariant testedVariant = null
206
207         for (BuildTypeData buildTypeData : buildTypes.values()) {
208
209             def variantConfig = new VariantConfiguration(
210                     extension.defaultConfig, getDefaultConfigData().androidSourceSet,
211                     buildTypeData.buildType, buildTypeData.androidSourceSet,
212                     VariantConfiguration.Type.DEFAULT)
213
214             variantConfig.addProductFlavor(productFlavorData.productFlavor,
215                     productFlavorData.androidSourceSet)
216
217             boolean isTestedVariant = (buildTypeData == testData)
218
219             ProductionAppVariant productionAppVariant = addVariant(variantConfig, null,
220                     isTestedVariant)
221
222             buildTypeData.assembleTask.dependsOn productionAppVariant.assembleTask
223             productFlavorData.assembleTask.dependsOn productionAppVariant.assembleTask
224
225             if (isTestedVariant) {
226                 testedVariantConfig = variantConfig
227                 testedVariant = productionAppVariant
228             }
229         }
230
231         assert testedVariantConfig != null && testedVariant != null
232
233         def variantTestConfig = new VariantConfiguration(
234                 extension.defaultConfig, getDefaultConfigData().androidTestSourceSet,
235                 testData.buildType, null,
236                 VariantConfiguration.Type.TEST)
237         variantTestConfig.addProductFlavor(productFlavorData.productFlavor,
238                 productFlavorData.androidTestSourceSet)
239
240         addTestVariant(variantTestConfig, testedVariantConfig, testedVariant)
241     }
242
243     /**
244      * Creates build tasks for a given variant.
245      * @param variantConfig
246      * @param assembleTask an optional assembleTask to be used. If null, a new one is created in the
247      *                     returned ProductAppVariant instance.
248      * @param isTestApk whether this apk is needed for running tests
249      * @return
250      */
251     private ProductionAppVariant addVariant(VariantConfiguration variantConfig, Task assembleTask,
252                                             boolean isTestApk) {
253
254         def variant = new ProductionAppVariant(variantConfig)
255
256         // Add a task to process the manifest(s)
257         def processManifestTask = project.tasks.add("process${variant.name}Manifest", ProcessManifest)
258         processManifestTask.plugin = this
259         processManifestTask.variant = variant
260         processManifestTask.conventionMapping.mergedManifest = { project.file("$project.buildDir/manifests/$variant.dirName/AndroidManifest.xml") }
261
262         // Add a task to crunch resource files
263         def crunchTask = project.tasks.add("crunchAndroid${variant.name}Res", CrunchResources)
264         crunchTask.plugin = this
265         crunchTask.variant = variant
266         crunchTask.conventionMapping.outputDir = { project.file("$project.buildDir/resources/$variant.dirName/res") }
267
268         // Add a task to create the BuildConfig class
269         def generateBuildConfigTask = project.tasks.add("generate${variant.name}BuildConfig", GenerateBuildConfigTask)
270         generateBuildConfigTask.plugin = this
271         generateBuildConfigTask.variant = variant
272         generateBuildConfigTask.conventionMapping.sourceOutputDir = { project.file("$project.buildDir/source/${variant.dirName}") }
273
274         // Add a task to generate resource source files
275         def processResources = project.tasks.add("processAndroid${variant.name}Res", ProcessResources)
276         processResources.dependsOn processManifestTask, crunchTask
277         processResources.plugin = this
278         processResources.variant = variant
279         processResources.conventionMapping.manifestFile = { processManifestTask.mergedManifest }
280         processResources.conventionMapping.crunchDir = { crunchTask.outputDir }
281         // TODO: unify with generateBuilderConfig somehow?
282         processResources.conventionMapping.sourceOutputDir = { project.file("$project.buildDir/source/$variant.dirName") }
283         processResources.conventionMapping.packageFile = { project.file("$project.buildDir/libs/${project.archivesBaseName}-${variant.baseName}.ap_") }
284         if (variantConfig.buildType.runProguard) {
285             processResources.conventionMapping.proguardFile = { project.file("$project.buildDir/proguard/${variant.dirName}/rules.txt") }
286         }
287         processResources.aaptOptions = extension.aaptOptions
288
289         // Add a compile task
290         def compileTaskName = "compile${variant.name}"
291         def compileTask = project.tasks.add(compileTaskName, Compile)
292         compileTask.dependsOn processResources, generateBuildConfigTask
293         if (variantConfig.hasFlavors()) {
294             compileTask.source(
295                     ((AndroidSourceSet)variantConfig.defaultSourceSet).sourceSet.java,
296                     ((AndroidSourceSet)variantConfig.buildTypeSourceSet).sourceSet.java,
297                     ((AndroidSourceSet)variantConfig.firstFlavorSourceSet).sourceSet.java,
298                     { processResources.sourceOutputDir })
299         } else {
300             compileTask.source(
301                     ((AndroidSourceSet)variantConfig.defaultSourceSet).sourceSet.java,
302                     ((AndroidSourceSet)variantConfig.buildTypeSourceSet).sourceSet.java,
303                     { processResources.sourceOutputDir })
304         }
305         // TODO: support classpath per flavor
306         compileTask.classpath = ((AndroidSourceSet)variantConfig.defaultSourceSet).sourceSet.compileClasspath
307         compileTask.conventionMapping.destinationDir = { project.file("$project.buildDir/classes/$variant.dirName") }
308         compileTask.doFirst {
309             compileTask.options.bootClasspath = getRuntimeJars(variant)
310         }
311
312         // Wire up the outputs
313         // TODO: remove the classpath once the jar deps are set in the Variantconfig, so that we can pre-dex
314         variant.runtimeClasspath = compileTask.outputs.files + compileTask.classpath
315         variant.resourcePackage = project.files({processResources.packageFile}) { builtBy processResources }
316         variant.compileTask = compileTask
317
318         Task returnTask = addPackageTasks(variant, assembleTask, isTestApk)
319         if (returnTask != null) {
320             variant.assembleTask = returnTask
321         }
322
323         return variant;
324     }
325
326     private void addTestVariant(VariantConfiguration variantConfig,
327                                 VariantConfiguration testedVariantConfig,
328                                 ProductionAppVariant testedVariant) {
329
330         def variant = new TestAppVariant(variantConfig, testedVariantConfig)
331
332         // Add a task to process the manifest
333         def processManifestTask = project.tasks.add("process${variant.name}Manifest", ProcessManifest)
334         processManifestTask.plugin = this
335         processManifestTask.variant = variant
336         processManifestTask.conventionMapping.mergedManifest = { project.file("$project.buildDir/manifests/$variant.dirName/AndroidManifest.xml") }
337
338         // Add a task to crunch resource files
339         def crunchTask = project.tasks.add("crunchAndroid${variant.name}Res", CrunchResources)
340         crunchTask.plugin = this
341         crunchTask.variant = variant
342         crunchTask.conventionMapping.outputDir = { project.file("$project.buildDir/resources/$variant.dirName/res") }
343
344         // Add a task to create the BuildConfig class
345         def generateBuildConfigTask = project.tasks.add("generate${variant.name}BuildConfig", GenerateBuildConfigTask)
346         generateBuildConfigTask.dependsOn processManifestTask // this is because the manifest can be generated.
347         generateBuildConfigTask.plugin = this
348         generateBuildConfigTask.variant = variant
349         generateBuildConfigTask.conventionMapping.sourceOutputDir = { project.file("$project.buildDir/source/${variant.dirName}") }
350
351         // Add a task to generate resource source files
352         def processResources = project.tasks.add("processAndroid${variant.name}Res", ProcessResources)
353         processResources.dependsOn processManifestTask, crunchTask
354         processResources.plugin = this
355         processResources.variant = variant
356         processResources.conventionMapping.manifestFile = { processManifestTask.mergedManifest }
357         processResources.conventionMapping.crunchDir = { crunchTask.outputDir }
358         // TODO: unify with generateBuilderConfig somehow?
359         processResources.conventionMapping.sourceOutputDir = { project.file("$project.buildDir/source/$variant.dirName") }
360         processResources.conventionMapping.packageFile = { project.file("$project.buildDir/libs/${project.archivesBaseName}-${variant.baseName}.ap_") }
361         processResources.aaptOptions = extension.aaptOptions
362
363         // Add a task to compile the test application
364         def testCompile = project.tasks.add("compile${variant.name}", Compile)
365         testCompile.dependsOn processResources, generateBuildConfigTask
366         if (variantConfig.hasFlavors()) {
367             testCompile.source(
368                     ((AndroidSourceSet)variantConfig.defaultSourceSet).sourceSet.java,
369                     ((AndroidSourceSet)variantConfig.firstFlavorSourceSet).sourceSet.java,
370                     { processResources.sourceOutputDir })
371         } else {
372             testCompile.source(
373                     ((AndroidSourceSet)variantConfig.defaultSourceSet).sourceSet.java,
374                     { processResources.sourceOutputDir })
375         }
376
377         testCompile.classpath = ((AndroidSourceSet)variantConfig.defaultSourceSet).sourceSet.compileClasspath + testedVariant.runtimeClasspath
378
379         testCompile.conventionMapping.destinationDir = { project.file("$project.buildDir/classes/$variant.dirName") }
380         testCompile.doFirst {
381             testCompile.options.bootClasspath = getRuntimeJars(variant)
382         }
383
384         variant.runtimeClasspath = testCompile.outputs.files
385         variant.resourcePackage = project.files({processResources.packageFile}) { builtBy processResources }
386         variant.compileTask = testCompile
387
388         Task assembleTask = addPackageTasks(variant, null, true /*isTestApk*/)
389
390         if (assembleTest != null) {
391             assembleTest.dependsOn assembleTask
392         }
393
394         def runTestsTask = project.tasks.add("Run${variant.name}Tests", RunTestsTask)
395         runTestsTask.sdkDir = sdkDir
396         runTestsTask.variant = variant
397
398         runAllTests.dependsOn runTestsTask
399     }
400
401     private Task addPackageTasks(ApplicationVariant variant, Task assembleTask, boolean isTestApk) {
402         // Add a dex task
403         def dexTaskName = "dex${variant.name}"
404         def dexTask = project.tasks.add(dexTaskName, Dex)
405         dexTask.dependsOn variant.compileTask
406         dexTask.plugin = this
407         dexTask.variant = variant
408         dexTask.conventionMapping.sourceFiles = { variant.runtimeClasspath }
409         dexTask.conventionMapping.outputFile = { project.file("${project.buildDir}/libs/${project.archivesBaseName}-${variant.baseName}.dex") }
410         dexTask.dexOptions = extension.dexOptions
411
412         // Add a task to generate application package
413         def packageApp = project.tasks.add("package${variant.name}", PackageApplication)
414         packageApp.dependsOn variant.resourcePackage, dexTask
415         packageApp.plugin = this
416         packageApp.variant = variant
417
418         def signedApk = variant.isSigned()
419
420         def apkName = signedApk ?
421             "${project.archivesBaseName}-${variant.baseName}-unaligned.apk" :
422             "${project.archivesBaseName}-${variant.baseName}-unsigned.apk"
423
424         packageApp.conventionMapping.outputFile = { project.file("$project.buildDir/apk/${apkName}") }
425         packageApp.conventionMapping.resourceFile = { variant.resourcePackage.singleFile }
426         packageApp.conventionMapping.dexFile = { dexTask.outputFile }
427
428         def appTask = packageApp
429
430         if (signedApk) {
431             if (variant.zipAlign) {
432                 // Add a task to zip align application package
433                 def alignApp = project.tasks.add("zipalign${variant.name}", ZipAlign)
434                 alignApp.dependsOn packageApp
435                 alignApp.conventionMapping.inputFile = { packageApp.outputFile }
436                 alignApp.conventionMapping.outputFile = { project.file("$project.buildDir/apk/${project.archivesBaseName}-${variant.baseName}.apk") }
437                 alignApp.sdkDir = sdkDir
438
439                 appTask = alignApp
440             }
441
442             // Add a task to install the application package
443             def installApp = project.tasks.add("install${variant.name}", InstallApplication)
444             installApp.description = "Installs the " + variant.description
445             installApp.group = "Install"
446             installApp.dependsOn appTask
447             installApp.conventionMapping.packageFile = { appTask.outputFile }
448             installApp.sdkDir = sdkDir
449
450             if (isTestApk) {
451                 installAllForTests.dependsOn installApp
452             }
453         }
454
455         // Add an assemble task
456         Task returnTask = null
457         if (assembleTask == null) {
458             assembleTask = project.tasks.add("assemble${variant.name}")
459             assembleTask.description = "Assembles the " + variant.description
460             assembleTask.group = "Build"
461             returnTask = assembleTask
462         }
463         assembleTask.dependsOn appTask
464
465         // add an uninstall task
466         def uninstallApp = project.tasks.add("uninstall${variant.name}", UninstallApplication)
467         uninstallApp.description = "Uninstalls the " + variant.description
468         uninstallApp.group = "Install"
469         uninstallApp.variant = variant
470         uninstallApp.sdkDir = sdkDir
471
472         uninstallAll.dependsOn uninstallApp
473
474         return returnTask
475     }
476
477     @Override
478     String getTarget() {
479         return extension.target;
480     }
481 }