Rework the way tests are installed and run.
[android/platform/tools/build.git] / gradle / src / main / groovy / com / android / build / gradle / AndroidBasePlugin.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.AndroidDependencyImpl
19 import com.android.build.gradle.internal.AndroidSourceSet
20 import com.android.build.gradle.internal.ApplicationVariant
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.AndroidBuilder
25 import com.android.builder.AndroidDependency
26 import com.android.builder.DefaultSdkParser
27 import com.android.builder.JarDependency
28 import com.android.builder.ProductFlavor
29 import com.android.builder.SdkParser
30 import com.android.builder.VariantConfiguration
31 import com.android.utils.ILogger
32 import org.gradle.api.DefaultTask
33 import org.gradle.api.GradleException
34 import org.gradle.api.Project
35 import org.gradle.api.Task
36 import org.gradle.api.artifacts.Configuration
37 import org.gradle.api.artifacts.ModuleVersionIdentifier
38 import org.gradle.api.artifacts.ProjectDependency
39 import org.gradle.api.artifacts.ResolvedDependency
40 import org.gradle.api.logging.LogLevel
41 import org.gradle.api.plugins.BasePlugin
42 import org.gradle.api.plugins.JavaBasePlugin
43 import org.gradle.api.tasks.SourceSet
44 import org.gradle.api.tasks.compile.Compile
45
46 /**
47  * Base class for all Android plugins
48  */
49 abstract class AndroidBasePlugin {
50
51     public final static String INSTALL_GROUP = "Install"
52
53     private final Map<Object, AndroidBuilder> builders = [:]
54
55     protected Project project
56     protected File sdkDir
57     private DefaultSdkParser androidSdkParser
58     private AndroidLogger androidLogger
59
60     private ProductFlavorData defaultConfigData
61     protected SourceSet mainSourceSet
62     protected SourceSet testSourceSet
63
64     protected Task uninstallAll
65     protected Task assembleTest
66
67     abstract String getTarget()
68
69     protected void apply(Project project) {
70         this.project = project
71         project.apply plugin: JavaBasePlugin
72
73         mainSourceSet = project.sourceSets.add("main")
74         testSourceSet = project.sourceSets.add("test")
75
76         project.tasks.assemble.description =
77             "Assembles all variants of all applications and secondary packages."
78
79         findSdk(project)
80
81         uninstallAll = project.tasks.add("uninstallAll")
82         uninstallAll.description = "Uninstall all applications."
83         uninstallAll.group = INSTALL_GROUP
84     }
85
86     protected setDefaultConfig(ProductFlavor defaultConfig) {
87         defaultConfigData = new ProductFlavorData(defaultConfig, mainSourceSet,
88                 testSourceSet, project)
89     }
90
91     ProductFlavorData getDefaultConfigData() {
92         return defaultConfigData
93     }
94
95     SdkParser getSdkParser() {
96         if (androidSdkParser == null) {
97             androidSdkParser = new DefaultSdkParser(sdkDir.absolutePath)
98         }
99
100         return androidSdkParser;
101     }
102
103     ILogger getLogger() {
104         if (androidLogger == null) {
105             androidLogger = new AndroidLogger(project.logger)
106         }
107
108         return androidLogger
109     }
110
111     boolean isVerbose() {
112         return project.logger.isEnabled(LogLevel.DEBUG)
113     }
114
115     AndroidBuilder getAndroidBuilder(Object key) {
116         return builders.get(key)
117     }
118
119     void setAndroidBuilder(Object key, AndroidBuilder androidBuilder) {
120         builders.put(key, androidBuilder)
121     }
122
123     private void findSdk(Project project) {
124         def localProperties = project.file("local.properties")
125         if (localProperties.exists()) {
126             Properties properties = new Properties()
127             localProperties.withInputStream { instr ->
128                 properties.load(instr)
129             }
130             def sdkDirProp = properties.getProperty('sdk.dir')
131             if (!sdkDirProp) {
132                 throw new RuntimeException("No sdk.dir property defined in local.properties file.")
133             }
134             sdkDir = new File(sdkDirProp)
135         } else {
136             def envVar = System.getenv("ANDROID_HOME")
137             if (envVar != null) {
138                 sdkDir = new File(envVar)
139             }
140         }
141
142         if (sdkDir == null) {
143             throw new RuntimeException(
144                     "SDK location not found. Define location with sdk.dir in the local.properties file or with an ANDROID_HOME environment variable.")
145         }
146
147         if (!sdkDir.directory) {
148             throw new RuntimeException(
149                     "The SDK directory '$sdkDir' specified in local.properties does not exist.")
150         }
151     }
152
153     protected String getRuntimeJars(ApplicationVariant variant) {
154         AndroidBuilder androidBuilder = getAndroidBuilder(variant)
155
156         return androidBuilder.runtimeClasspath.join(":")
157     }
158
159     protected ProcessManifestTask createProcessManifestTask(ApplicationVariant variant,
160                                                         String manifestOurDir) {
161         def processManifestTask = project.tasks.add("process${variant.name}Manifest",
162                 ProcessManifestTask)
163         processManifestTask.plugin = this
164         processManifestTask.variant = variant
165         processManifestTask.configObjects = variant.configObjects
166         processManifestTask.conventionMapping.inputManifests = { variant.config.manifestInputs }
167         processManifestTask.conventionMapping.processedManifest = {
168             project.file(
169                     "$project.buildDir/${manifestOurDir}/$variant.dirName/AndroidManifest.xml")
170         }
171
172         return processManifestTask
173     }
174
175     protected CrunchResourcesTask createCrunchResTask(ApplicationVariant variant) {
176         def crunchTask = project.tasks.add("crunch${variant.name}Res", CrunchResourcesTask)
177         crunchTask.plugin = this
178         crunchTask.variant = variant
179         crunchTask.configObjects = variant.configObjects
180         crunchTask.conventionMapping.resDirectories = { variant.config.resourceInputs }
181         crunchTask.conventionMapping.outputDir = {
182             project.file("$project.buildDir/res/$variant.dirName")
183         }
184
185         return crunchTask
186     }
187
188     protected GenerateBuildConfigTask createBuildConfigTask(ApplicationVariant variant,
189                                                             ProcessManifestTask processManifestTask) {
190         def generateBuildConfigTask = project.tasks.add(
191                 "generate${variant.name}BuildConfig", GenerateBuildConfigTask)
192         if (processManifestTask != null) {
193             // This is in case the manifest is generated
194             generateBuildConfigTask.dependsOn processManifestTask
195         }
196         generateBuildConfigTask.plugin = this
197         generateBuildConfigTask.variant = variant
198         generateBuildConfigTask.configObjects = variant.configObjects
199         generateBuildConfigTask.optionalJavaLines = variant.buildConfigLines
200         generateBuildConfigTask.conventionMapping.sourceOutputDir = {
201             project.file("$project.buildDir/source/${variant.dirName}")
202         }
203         return generateBuildConfigTask
204     }
205
206     protected ProcessResourcesTask createProcessResTask(ApplicationVariant variant,
207                                                     ProcessManifestTask processManifestTask,
208                                                     CrunchResourcesTask crunchTask) {
209         def processResources = project.tasks.add("process${variant.name}Res", ProcessResourcesTask)
210         processResources.dependsOn processManifestTask
211         processResources.plugin = this
212         processResources.variant = variant
213         processResources.configObjects = variant.configObjects
214         processResources.conventionMapping.manifestFile = { processManifestTask.processedManifest }
215         // TODO: unify with generateBuilderConfig, and compileAidl somehow?
216         processResources.conventionMapping.sourceOutputDir = {
217             project.file("$project.buildDir/source/$variant.dirName")
218         }
219         processResources.conventionMapping.packageFile = {
220             project.file(
221                     "$project.buildDir/libs/${project.archivesBaseName}-${variant.baseName}.ap_")
222         }
223         if (variant.runProguard) {
224             processResources.conventionMapping.proguardFile = {
225                 project.file("$project.buildDir/proguard/${variant.dirName}/rules.txt")
226             }
227         }
228
229         if (crunchTask != null) {
230             processResources.dependsOn crunchTask
231             processResources.conventionMapping.crunchDir = { crunchTask.outputDir }
232             processResources.conventionMapping.resDirectories = { crunchTask.resDirectories }
233         } else {
234             processResources.conventionMapping.resDirectories = { variant.config.resourceInputs }
235         }
236
237         processResources.aaptOptions = extension.aaptOptions
238         return processResources
239     }
240
241     protected CompileAidlTask createAidlTask(ApplicationVariant variant) {
242
243         VariantConfiguration config = variant.config
244
245         def compileTask = project.tasks.add("compile${variant.name}Aidl", CompileAidlTask)
246         compileTask.plugin = this
247         compileTask.variant = variant
248         compileTask.configObjects = variant.configObjects
249
250         List<Object> sourceList = new ArrayList<Object>();
251         sourceList.add(config.defaultSourceSet.aidlSource)
252         if (config.getType() != VariantConfiguration.Type.TEST) {
253             sourceList.add(config.buildTypeSourceSet.aidlSource)
254         }
255         if (config.hasFlavors()) {
256             for (com.android.builder.SourceSet flavorSourceSet : config.flavorSourceSets) {
257                 sourceList.add(flavorSourceSet.aidlSource)
258             }
259         }
260
261         compileTask.sourceDirs = sourceList
262         compileTask.importDirs = variant.config.aidlImports
263
264         compileTask.conventionMapping.sourceOutputDir = {
265             project.file("$project.buildDir/source/$variant.dirName")
266         }
267
268         return compileTask
269     }
270
271     protected void createCompileTask(ApplicationVariant variant,
272                                      ApplicationVariant testedVariant,
273                                      ProcessResourcesTask processResources,
274                                      GenerateBuildConfigTask generateBuildConfigTask,
275                                      CompileAidlTask aidlTask) {
276         def compileTask = project.tasks.add("compile${variant.name}", Compile)
277         compileTask.dependsOn processResources, generateBuildConfigTask, aidlTask
278
279         VariantConfiguration config = variant.config
280
281         List<Object> sourceList = new ArrayList<Object>();
282         sourceList.add(((AndroidSourceSet) config.defaultSourceSet).sourceSet.java)
283         sourceList.add({ processResources.sourceOutputDir })
284         if (config.getType() != VariantConfiguration.Type.TEST) {
285             sourceList.add(((AndroidSourceSet) config.buildTypeSourceSet).sourceSet.java)
286         }
287         if (config.hasFlavors()) {
288             for (com.android.builder.SourceSet flavorSourceSet : config.flavorSourceSets) {
289                 sourceList.add(((AndroidSourceSet) flavorSourceSet).sourceSet.java)
290             }
291         }
292         compileTask.source = sourceList.toArray()
293
294         // if the test is for a full app, the tested runtimeClasspath is added to the classpath for
295         // compilation only, not for packaging
296         variant.packagedClasspath = project.files({config.compileClasspath})
297         if (testedVariant != null &&
298                 testedVariant.config.type != VariantConfiguration.Type.LIBRARY) {
299             compileTask.classpath = variant.packagedClasspath + testedVariant.runtimeClasspath
300         } else {
301             compileTask.classpath = variant.packagedClasspath
302         }
303         // TODO - dependency information for the compile classpath is being lost.
304         // Add a temporary approximation
305         compileTask.dependsOn project.configurations.compile.buildDependencies
306
307         compileTask.conventionMapping.destinationDir = {
308             project.file("$project.buildDir/classes/$variant.dirName")
309         }
310         compileTask.doFirst {
311             compileTask.options.bootClasspath = getRuntimeJars(variant)
312         }
313
314         // Wire up the outputs
315         variant.runtimeClasspath = compileTask.outputs.files + compileTask.classpath
316         variant.resourcePackage = project.files({processResources.packageFile}) {
317             builtBy processResources
318         }
319         variant.compileTask = compileTask
320     }
321
322     protected void createTestTasks(TestAppVariant variant, ProductionAppVariant testedVariant) {
323         // Add a task to process the manifest
324         def processManifestTask = createProcessManifestTask(variant, "manifests")
325
326         // Add a task to crunch resource files
327         def crunchTask = createCrunchResTask(variant)
328
329         if (testedVariant.config.type == VariantConfiguration.Type.LIBRARY) {
330             // in this case the tested library must be fully built before test can be built!
331             if (testedVariant.assembleTask != null) {
332                 processManifestTask.dependsOn testedVariant.assembleTask
333                 crunchTask.dependsOn testedVariant.assembleTask
334             }
335         }
336
337         // Add a task to create the BuildConfig class
338         def generateBuildConfigTask = createBuildConfigTask(variant, processManifestTask)
339
340         // Add a task to generate resource source files
341         def processResources = createProcessResTask(variant, processManifestTask, crunchTask)
342
343         def compileAidl = createAidlTask(variant)
344
345         // Add a task to compile the test application
346         createCompileTask(variant, testedVariant, processResources, generateBuildConfigTask,
347                 compileAidl)
348
349         addPackageTasks(variant, null)
350
351         if (assembleTest != null) {
352             assembleTest.dependsOn variant.assembleTask
353         }
354
355         // create the check task for this test
356         def checkTask = project.tasks.add("check${testedVariant.name}", DefaultTask)
357         checkTask.description = "Installs and runs the checks for Build ${testedVariant.name}."
358         checkTask.group = JavaBasePlugin.VERIFICATION_GROUP
359
360         checkTask.dependsOn testedVariant.assembleTask, variant.assembleTask
361         project.tasks.check.dependsOn checkTask
362
363         // now run the test.
364         def runTestsTask = project.tasks.add("run${testedVariant.name}Tests", RunTestsTask)
365         runTestsTask.description = "Runs the checks for Build ${testedVariant.name}. Must be installed on device."
366         runTestsTask.group = JavaBasePlugin.VERIFICATION_GROUP
367         runTestsTask.sdkDir = sdkDir
368         runTestsTask.variant = variant
369         checkTask.doLast { runTestsTask }
370
371         // TODO: don't rely on dependsOn which isn't reliable for execution order.
372         if (testedVariant.config.type == VariantConfiguration.Type.DEFAULT) {
373             checkTask.dependsOn testedVariant.installTask, variant.installTask, runTestsTask, testedVariant.uninstallTask, variant.uninstallTask
374         } else {
375             checkTask.dependsOn variant.installTask, runTestsTask, variant.uninstallTask
376         }
377     }
378
379     /**
380      * Creates the packaging tasks for the given Variant.
381      * @param variant the variant.
382      * @param assembleTask an optional assembleTask to be used. If null a new one is created. The
383      *                assembleTask is always set in the Variant.
384      */
385     protected void addPackageTasks(ApplicationVariant variant, Task assembleTask) {
386         // Add a dex task
387         def dexTaskName = "dex${variant.name}"
388         def dexTask = project.tasks.add(dexTaskName, DexTask)
389         dexTask.dependsOn variant.compileTask
390         dexTask.plugin = this
391         dexTask.variant = variant
392         dexTask.conventionMapping.libraries = { variant.packagedClasspath }
393         dexTask.conventionMapping.sourceFiles = { variant.compileTask.outputs.files }
394         dexTask.conventionMapping.outputFile = {
395             project.file(
396                     "${project.buildDir}/libs/${project.archivesBaseName}-${variant.baseName}.dex")
397         }
398         dexTask.dexOptions = extension.dexOptions
399
400         // Add a task to generate application package
401         def packageApp = project.tasks.add("package${variant.name}", PackageApplicationTask)
402         packageApp.dependsOn variant.resourcePackage, dexTask
403         packageApp.plugin = this
404         packageApp.variant = variant
405         packageApp.configObjects = variant.configObjects
406
407         def signedApk = variant.isSigned()
408
409         def apkName = signedApk ?
410             "${project.archivesBaseName}-${variant.baseName}-unaligned.apk" :
411             "${project.archivesBaseName}-${variant.baseName}-unsigned.apk"
412
413         packageApp.conventionMapping.outputFile = {
414             project.file("$project.buildDir/apk/${apkName}")
415         }
416         packageApp.conventionMapping.resourceFile = { variant.resourcePackage.singleFile }
417         packageApp.conventionMapping.dexFile = { dexTask.outputFile }
418
419         def appTask = packageApp
420
421         if (signedApk) {
422             if (variant.zipAlign) {
423                 // Add a task to zip align application package
424                 def alignApp = project.tasks.add("zipalign${variant.name}", ZipAlignTask)
425                 alignApp.dependsOn packageApp
426                 alignApp.conventionMapping.inputFile = { packageApp.outputFile }
427                 alignApp.conventionMapping.outputFile = {
428                     project.file(
429                             "$project.buildDir/apk/${project.archivesBaseName}-${variant.baseName}.apk")
430                 }
431                 alignApp.sdkDir = sdkDir
432
433                 appTask = alignApp
434             }
435
436             // Add a task to install the application package
437             def installTask = project.tasks.add("install${variant.name}", InstallTask)
438             installTask.description = "Installs the " + variant.description
439             installTask.group = INSTALL_GROUP
440             installTask.dependsOn appTask
441             installTask.conventionMapping.packageFile = { appTask.outputFile }
442             installTask.sdkDir = sdkDir
443
444             variant.installTask = installTask
445         }
446
447         // Add an assemble task
448         if (assembleTask == null) {
449             assembleTask = project.tasks.add("assemble${variant.name}")
450             assembleTask.description = "Assembles the " + variant.description
451             assembleTask.group = BasePlugin.BUILD_GROUP
452         }
453         assembleTask.dependsOn appTask
454         variant.assembleTask = assembleTask
455
456         // add an uninstall task
457         def uninstallTask = project.tasks.add("uninstall${variant.name}", UninstallTask)
458         uninstallTask.description = "Uninstalls the " + variant.description
459         uninstallTask.group = INSTALL_GROUP
460         uninstallTask.variant = variant
461         uninstallTask.sdkDir = sdkDir
462
463         variant.uninstallTask = uninstallTask
464         uninstallAll.dependsOn uninstallTask
465     }
466
467     PrepareDependenciesTask createPrepareDependenciesTask(ProductionAppVariant variant) {
468         // TODO - include variant specific dependencies too
469         def compileClasspath = project.configurations.compile
470
471         // TODO - shouldn't need to do this - fix this in Gradle
472         ensureConfigured(compileClasspath)
473
474         def prepareDependenciesTask = project.tasks.add("prepare${variant.name}Dependencies",
475                 PrepareDependenciesTask)
476
477         // TODO - should be able to infer this
478         prepareDependenciesTask.dependsOn compileClasspath
479
480         // TODO - defer downloading until required
481         // TODO - build the library dependency graph
482         List<AndroidDependency> bundles = []
483         List<JarDependency> jars = []
484         Map<ModuleVersionIdentifier, Object> modules = [:]
485         compileClasspath.resolvedConfiguration.firstLevelModuleDependencies.each { ResolvedDependency dependency ->
486             addDependency(dependency, prepareDependenciesTask, bundles, jars, modules)
487         }
488
489         variant.config.androidDependencies = bundles
490         variant.config.jarDependencies = jars
491
492         // TODO - filter bundles out of source set classpath
493
494         return prepareDependenciesTask
495     }
496
497     def ensureConfigured(Configuration config) {
498         config.allDependencies.withType(ProjectDependency).each { dep ->
499             project.evaluationDependsOn(dep.dependencyProject.path)
500             ensureConfigured(dep.projectConfiguration)
501         }
502     }
503
504     def addDependency(ResolvedDependency dependency, PrepareDependenciesTask prepareDependenciesTask, Collection<AndroidDependency> bundles, Collection<JarDependency> jars, Map<ModuleVersionIdentifier, Object> modules) {
505         def id = dependency.module.id
506         List<AndroidDependency> bundlesForThisModule = modules[id]
507         if (bundlesForThisModule == null) {
508             bundlesForThisModule = []
509             modules[id] = bundlesForThisModule
510
511             def nestedBundles = []
512             dependency.children.each { ResolvedDependency child ->
513                 addDependency(child, prepareDependenciesTask, nestedBundles, jars, modules)
514             }
515
516             dependency.moduleArtifacts.each { artifact ->
517                 if (artifact.type == 'alb') {
518                     def explodedDir = project.file("$project.buildDir/exploded-bundles/$artifact.file.name")
519                     bundlesForThisModule << new AndroidDependencyImpl(explodedDir, nestedBundles)
520                     prepareDependenciesTask.add(artifact.file, explodedDir)
521                 } else {
522                     // TODO - need the correct values for the boolean flags
523                     jars << new JarDependency(artifact.file.absolutePath, true, true, true)
524                 }
525             }
526
527             if (bundlesForThisModule.empty && !nestedBundles.empty) {
528                 throw new GradleException("Module version $id depends on libraries but is not a library itself")
529             }
530         }
531
532         bundles.addAll(bundlesForThisModule)
533     }
534
535 }