3d49b75c2894189d647dad4f1099db57bf3f112d
[android/platform/tools/build.git] / gradle / src / main / groovy / com / android / build / gradle / AppPlugin.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.BuildTypeData
19 import com.android.build.gradle.internal.BuildTypeDsl
20 import com.android.build.gradle.internal.ProductFlavorData
21 import com.android.build.gradle.internal.ProductFlavorDsl
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.VariantConfiguration
26 import com.google.common.collect.ArrayListMultimap
27 import com.google.common.collect.ListMultimap
28 import org.gradle.api.Project
29 import org.gradle.api.Task
30 import org.gradle.api.internal.project.ProjectInternal
31 import org.gradle.api.plugins.BasePlugin
32
33 class AppPlugin extends com.android.build.gradle.BasePlugin implements org.gradle.api.Plugin<Project> {
34     private final Map<String, BuildTypeData> buildTypes = [:]
35     private final Map<String, ProductFlavorData> productFlavors = [:]
36
37     private AppExtension extension
38
39     @Override
40     void apply(Project project) {
41         super.apply(project)
42
43         def buildTypeContainer = project.container(BuildTypeDsl)
44         def productFlavorContainer = project.container(ProductFlavorDsl)
45
46         extension = project.extensions.create('android', AppExtension,
47                 (ProjectInternal) project, buildTypeContainer, productFlavorContainer)
48         setDefaultConfig(extension.defaultConfig, extension.sourceSetsContainer)
49
50         buildTypeContainer.whenObjectAdded { BuildType buildType ->
51             addBuildType(buildType)
52         }
53         buildTypeContainer.whenObjectRemoved {
54             throw new UnsupportedOperationException("Removing build types is not implemented yet.")
55         }
56
57         buildTypeContainer.create(BuildType.DEBUG)
58         buildTypeContainer.create(BuildType.RELEASE)
59
60         productFlavorContainer.whenObjectAdded { ProductFlavorDsl productFlavor ->
61             addProductFlavor(productFlavor)
62         }
63
64         productFlavorContainer.whenObjectRemoved {
65             throw new UnsupportedOperationException(
66                     "Removing product flavors is not implemented yet.")
67         }
68
69         project.afterEvaluate {
70             createAndroidTasks()
71         }
72     }
73
74     private void addBuildType(BuildType buildType) {
75         if (buildType.name.startsWith("test")) {
76             throw new RuntimeException("BuildType names cannot start with 'test'")
77         }
78         if (productFlavors.containsKey(buildType.name)) {
79             throw new RuntimeException("BuildType names cannot collide with ProductFlavor names")
80         }
81
82         def sourceSet = extension.sourceSetsContainer.create(buildType.name)
83
84         BuildTypeData buildTypeData = new BuildTypeData(buildType, sourceSet, project)
85         project.tasks.assemble.dependsOn buildTypeData.assembleTask
86
87         buildTypes[buildType.name] = buildTypeData
88     }
89
90     private void addProductFlavor(ProductFlavorDsl productFlavor) {
91         if (productFlavor.name.startsWith("test")) {
92             throw new RuntimeException("ProductFlavor names cannot start with 'test'")
93         }
94         if (buildTypes.containsKey(productFlavor.name)) {
95             throw new RuntimeException("ProductFlavor names cannot collide with BuildType names")
96         }
97
98         def mainSourceSet = extension.sourceSetsContainer.create(productFlavor.name)
99         String testName = "test${productFlavor.name.capitalize()}"
100         def testSourceSet = extension.sourceSetsContainer.create(testName)
101
102         ProductFlavorData productFlavorData = new ProductFlavorData(
103                 productFlavor, mainSourceSet, testSourceSet, project)
104
105         productFlavors[productFlavor.name] = productFlavorData
106     }
107
108     private void createAndroidTasks() {
109         if (productFlavors.isEmpty()) {
110             createTasksForDefaultBuild()
111         } else {
112             // there'll be more than one test app, so we need a top level assembleTest
113             assembleTest = project.tasks.add("assembleTest")
114             assembleTest.group = BasePlugin.BUILD_GROUP
115             assembleTest.description = "Assembles all the Test applications"
116
117             // check whether we have multi flavor builds
118             if (extension.flavorGroupList == null || extension.flavorGroupList.size() < 2) {
119                 productFlavors.values().each { ProductFlavorData productFlavorData ->
120                     createTasksForFlavoredBuild(productFlavorData)
121                 }
122             } else {
123                 // need to group the flavor per group.
124                 // First a map of group -> list(ProductFlavor)
125                 ArrayListMultimap<String, ProductFlavorData> map = ArrayListMultimap.create();
126                 productFlavors.values().each { ProductFlavorData productFlavorData ->
127                     def flavor = productFlavorData.productFlavor
128                     if (flavor.flavorGroup == null) {
129                         throw new RuntimeException(
130                                 "Flavor ${flavor.name} has no flavor group.")
131                     }
132                     if (!extension.flavorGroupList.contains(flavor.flavorGroup)) {
133                         throw new RuntimeException(
134                                 "Flavor ${flavor.name} has unknown group ${flavor.flavorGroup}.")
135                     }
136
137                     map.put(flavor.flavorGroup, productFlavorData)
138                 }
139
140                 // now we use the flavor groups to generate an ordered array of flavor to use
141                 ProductFlavorData[] array = new ProductFlavorData[extension.flavorGroupList.size()]
142                 createTasksForMultiFlavoredBuilds(array, 0, map)
143             }
144         }
145     }
146
147     private createTasksForMultiFlavoredBuilds(ProductFlavorData[] datas, int i,
148                                               ListMultimap<String, ProductFlavorData> map) {
149         if (i == datas.length) {
150             createTasksForFlavoredBuild(datas)
151             return
152         }
153
154         // fill the array at the current index
155         def group = extension.flavorGroupList.get(i)
156         def flavorList = map.get(group)
157         for (ProductFlavorData flavor : flavorList) {
158            datas[i] = flavor
159             createTasksForMultiFlavoredBuilds(datas, i+1, map)
160         }
161     }
162
163     /**
164      * Creates Tasks for non-flavored build. This means assembleDebug, assembleRelease, and other
165      * assemble<Type> are directly building the <type> build instead of all build of the given
166      * <type>.
167      */
168     private createTasksForDefaultBuild() {
169
170         BuildTypeData testData = buildTypes[extension.testBuildType]
171         if (testData == null) {
172             throw new RuntimeException("Test Build Type '$extension.testBuildType' does not exist.")
173         }
174
175         ProductionAppVariant testedVariant = null
176
177         ProductFlavorData defaultConfigData = getDefaultConfigData();
178
179         for (BuildTypeData buildTypeData : buildTypes.values()) {
180
181             def variantConfig = new VariantConfiguration(
182                     defaultConfigData.productFlavor, defaultConfigData.sourceSet,
183                     buildTypeData.buildType, buildTypeData.sourceSet)
184
185             ProductionAppVariant productionAppVariant = addVariant(variantConfig,
186                     buildTypeData.assembleTask)
187
188             if (buildTypeData == testData) {
189                 testedVariant = productionAppVariant
190             }
191         }
192
193         assert testedVariant != null
194
195         def testVariantConfig = new VariantConfiguration(
196                 defaultConfigData.productFlavor, defaultConfigData.testSourceSet,
197                 testData.buildType, null,
198                 VariantConfiguration.Type.TEST, testedVariant.config)
199
200         // TODO: add actual dependencies
201         testVariantConfig.setAndroidDependencies(null)
202
203         def testVariant = new TestAppVariant(testVariantConfig)
204         createTestTasks(testVariant, testedVariant)
205     }
206
207     /**
208      * Creates Task for a given flavor. This will create tasks for all build types for the given
209      * flavor.
210      * @param flavorDataList the flavor(s) to build.
211      */
212     private createTasksForFlavoredBuild(ProductFlavorData... flavorDataList) {
213
214         BuildTypeData testData = buildTypes[extension.testBuildType]
215         if (testData == null) {
216             throw new RuntimeException("Test Build Type '$extension.testBuildType' does not exist.")
217         }
218
219         ProductionAppVariant testedVariant = null
220
221         // assembleTask for this flavor(group)
222         def assembleTask
223
224         for (BuildTypeData buildTypeData : buildTypes.values()) {
225
226             def variantConfig = new VariantConfiguration(
227                     extension.defaultConfig, getDefaultConfigData().sourceSet,
228                     buildTypeData.buildType, buildTypeData.sourceSet)
229
230             for (ProductFlavorData data : flavorDataList) {
231                 variantConfig.addProductFlavor(data.productFlavor, data.sourceSet)
232             }
233
234             ProductionAppVariant productionAppVariant = addVariant(variantConfig, null)
235
236             buildTypeData.assembleTask.dependsOn productionAppVariant.assembleTask
237
238             if (assembleTask == null) {
239                 // create the task based on the name of the flavors.
240                 assembleTask = createAssembleTask(flavorDataList)
241                 project.tasks.assemble.dependsOn assembleTask
242             }
243             assembleTask.dependsOn productionAppVariant.assembleTask
244
245             if (buildTypeData == testData) {
246                 testedVariant = productionAppVariant
247             }
248         }
249
250         assert testedVariant != null
251
252         def testVariantConfig = new VariantConfiguration(
253                 extension.defaultConfig, getDefaultConfigData().testSourceSet,
254                 testData.buildType, null,
255                 VariantConfiguration.Type.TEST, testedVariant.config)
256
257         for (ProductFlavorData data : flavorDataList) {
258             testVariantConfig.addProductFlavor(data.productFlavor, data.testSourceSet)
259         }
260
261         // TODO: add actual dependencies
262         testVariantConfig.setAndroidDependencies(null)
263
264         def testVariant = new TestAppVariant(testVariantConfig)
265         createTestTasks(testVariant, testedVariant)
266     }
267
268     private Task createAssembleTask(ProductFlavorData[] flavorDataList) {
269         def name = ProductFlavorData.getFlavoredName(flavorDataList, true)
270
271         def assembleTask = project.tasks.add("assemble${name}")
272         assembleTask.description = "Assembles all builds for flavor ${name}"
273         assembleTask.group = "Build"
274
275         return assembleTask
276     }
277
278     /**
279      * Creates build tasks for a given variant.
280      * @param variantConfig
281      * @param assembleTask an optional assembleTask to be used. If null, a new one is created.
282      * @return
283      */
284     private ProductionAppVariant addVariant(VariantConfiguration variantConfig, Task assembleTask) {
285
286         def variant = new ProductionAppVariant(variantConfig)
287
288         def prepareDependenciesTask = createPrepareDependenciesTask(variant)
289
290         // Add a task to process the manifest(s)
291         def processManifestTask = createProcessManifestTask(variant, "manifests")
292         // TODO - move this
293         processManifestTask.dependsOn prepareDependenciesTask
294
295         // Add a task to crunch resource files
296         def crunchTask = createCrunchResTask(variant)
297
298         // Add a task to create the BuildConfig class
299         def generateBuildConfigTask = createBuildConfigTask(variant, null)
300
301         // Add a task to generate resource source files
302         def processResources = createProcessResTask(variant, processManifestTask, crunchTask)
303
304         // Add a task to process the java resources
305         createProcessJavaResTask(variant)
306
307         def compileAidl = createAidlTask(variant)
308         // TODO - move this
309         compileAidl.dependsOn prepareDependenciesTask
310
311         // Add a compile task
312         createCompileTask(variant, null/*testedVariant*/, processResources, generateBuildConfigTask,
313                 compileAidl)
314
315         addPackageTasks(variant, assembleTask)
316
317         return variant;
318     }
319
320     @Override
321     String getTarget() {
322         return extension.target;
323     }
324 }