1 /*
<lambda>null2  * Copyright 2019 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 
17 package androidx.build
18 
19 import com.android.build.api.variant.LintLifecycleExtension
20 import com.android.build.gradle.AppPlugin
21 import com.android.build.gradle.LibraryPlugin
22 import java.io.File
23 import org.gradle.api.Plugin
24 import org.gradle.api.Project
25 import org.gradle.api.artifacts.type.ArtifactTypeDefinition
26 import org.gradle.api.attributes.Attribute
27 import org.gradle.api.tasks.bundling.Zip
28 import org.gradle.kotlin.dsl.create
29 import org.jetbrains.kotlin.gradle.plugin.CompilerPluginConfig
30 import org.jetbrains.kotlin.gradle.plugin.KotlinBasePluginWrapper
31 import org.jetbrains.kotlin.gradle.plugin.SubpluginOption
32 import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
33 
34 const val zipComposeReportsTaskName = "zipComposeCompilerReports"
35 const val zipComposeMetricsTaskName = "zipComposeCompilerMetrics"
36 
37 /** Plugin to apply common configuration for Compose projects. */
38 class AndroidXComposeImplPlugin : Plugin<Project> {
39     override fun apply(project: Project) {
40         val extension =
41             project.extensions.create<AndroidXComposeExtension>("androidxCompose", project)
42         project.plugins.configureEach { plugin ->
43             when (plugin) {
44                 is AppPlugin,
45                 is LibraryPlugin -> {
46                     project.configureAndroidCommonOptions()
47                 }
48                 is KotlinBasePluginWrapper -> {
49                     configureComposeCompilerPlugin(project, extension)
50                 }
51             }
52         }
53     }
54 
55     companion object {
56         private fun Project.configureAndroidCommonOptions() {
57             extensions.findByType(LintLifecycleExtension::class.java)!!.finalizeDsl { lint ->
58                 val isPublished = androidXExtension.shouldPublish()
59 
60                 lint.run {
61                     // These lint checks are normally a warning (or lower), but we ignore (in
62                     // AndroidX)
63                     // warnings in Lint, so we make it an error here so it will fail the build.
64                     // Note that this causes 'UnknownIssueId' lint warnings in the build log when
65                     // Lint tries to apply this rule to modules that do not have this lint check, so
66                     // we disable that check too
67                     disable.add("UnknownIssueId")
68                     error.addAll(ComposeLintWarningIdsToTreatAsErrors)
69 
70                     // Paths we want to disable ListIteratorChecks for
71                     val ignoreListIteratorFilter =
72                         listOf(
73                             // These are not runtime libraries and so Iterator allocation is not
74                             // relevant.
75                             "compose:ui:ui-test",
76                             "compose:ui:ui-tooling",
77                             "compose:ui:ui-inspection",
78                             // Navigation libraries are not in performance critical paths, so we can
79                             // ignore them.
80                             "navigation:navigation-compose",
81                             "wear:compose:compose-navigation"
82                         )
83 
84                     // Disable ListIterator if we are not in a matching path, or we are in an
85                     // unpublished project
86                     if (ignoreListIteratorFilter.any { path.contains(it) } || !isPublished) {
87                         disable.add("ListIterator")
88                     }
89 
90                     // b/333784604 Disable ConfigurationScreenWidthHeight for wear libraries, it
91                     // does not apply to wear
92                     if (path.startsWith(":wear:")) {
93                         disable.add("ConfigurationScreenWidthHeight")
94                     }
95 
96                     // These checks are not required for samples projects.
97                     if (androidXExtension.type == SoftwareType.SAMPLES) {
98                         disable.add("ListIterator")
99                         disable.add("PrimitiveInCollection")
100                     }
101                 }
102             }
103 
104             if (!allowMissingLintProject()) {
105                 // TODO: figure out how to apply this to multiplatform modules
106                 dependencies.add(
107                     "lintChecks",
108                     project.dependencies.project(
109                         mapOf(
110                             "path" to ":compose:lint:internal-lint-checks",
111                             // TODO(b/206617878) remove this shadow configuration
112                             "configuration" to "shadow"
113                         )
114                     )
115                 )
116             }
117         }
118     }
119 }
120 
121 private const val COMPILER_PLUGIN_CONFIGURATION = "kotlinPlugin"
122 
configureComposeCompilerPluginnull123 private fun configureComposeCompilerPlugin(project: Project, extension: AndroidXComposeExtension) {
124     project.afterEvaluate {
125         // If a project has opted-out of Compose compiler plugin, don't add it
126         if (!extension.composeCompilerPluginEnabled) return@afterEvaluate
127 
128         // Create configuration that we'll use to load Compose compiler plugin
129         val configuration =
130             project.configurations.create(COMPILER_PLUGIN_CONFIGURATION) {
131                 it.isCanBeConsumed = false
132             }
133         // Add Compose compiler plugin to kotlinPlugin configuration, making sure it works
134         // for Playground builds as well
135         val isPlayground = ProjectLayoutType.isPlayground(project)
136         val compilerPluginVersion = project.getVersionByName("kotlin")
137         project.dependencies.add(
138             COMPILER_PLUGIN_CONFIGURATION,
139             "org.jetbrains.kotlin:kotlin-compose-compiler-plugin-embeddable:$compilerPluginVersion"
140         )
141 
142         if (
143             !isPlayground &&
144                 // ksp is also a compiler plugin, updating Kotlin for it will likely break the build
145                 !project.plugins.hasPlugin("com.google.devtools.ksp")
146         ) {
147             if (compilerPluginVersion.endsWith("-SNAPSHOT")) {
148                 // use exact project path instead of subprojects.find, it is faster
149                 val compilerProject = project.rootProject.resolveProject(":compose")
150                 val compilerMavenDirectory =
151                     File(
152                         compilerProject.projectDir,
153                         "compiler/compose-compiler-snapshot-repository"
154                     )
155                 project.repositories.maven { it.url = compilerMavenDirectory.toURI() }
156                 project.configurations.configureEach {
157                     it.resolutionStrategy.eachDependency { dep ->
158                         val requested = dep.requested
159                         if (
160                             requested.group == "org.jetbrains.kotlin" &&
161                                 (requested.name == "kotlin-compiler-embeddable" ||
162                                     requested.name == "kotlin-compose-compiler-plugin-embeddable")
163                         ) {
164                             dep.useVersion(compilerPluginVersion)
165                         }
166                     }
167                 }
168             }
169         }
170 
171         val kotlinPluginProvider =
172             project.provider {
173                 configuration.incoming
174                     .artifactView { view ->
175                         view.attributes { attributes ->
176                             attributes.attribute(
177                                 Attribute.of("artifactType", String::class.java),
178                                 ArtifactTypeDefinition.JAR_TYPE
179                             )
180                         }
181                     }
182                     .files
183             }
184 
185         val enableMetrics = project.enableComposeCompilerMetrics()
186         val enableReports = project.enableComposeCompilerReports()
187 
188         val compileTasks = project.tasks.withType(KotlinCompile::class.java)
189 
190         compileTasks.configureEach { compile ->
191             compile.inputs.property("composeMetricsEnabled", enableMetrics)
192             compile.inputs.property("composeReportsEnabled", enableReports)
193 
194             compile.pluginClasspath.from(kotlinPluginProvider.get())
195 
196             compile.enableFeatureFlag(ComposeFeatureFlag.OptimizeNonSkippingGroups)
197             compile.enableFeatureFlag(ComposeFeatureFlag.PausableComposition)
198 
199             compile.addPluginOption(ComposeCompileOptions.SourceOption, "true")
200         }
201 
202         if (enableMetrics) {
203             project.rootProject.tasks.named(zipComposeMetricsTaskName).configure { zipTask ->
204                 zipTask.dependsOn(compileTasks)
205             }
206 
207             val metricsIntermediateDir = project.compilerMetricsIntermediatesDir()
208             compileTasks.configureEach { compile ->
209                 compile.addPluginOption(
210                     ComposeCompileOptions.MetricsOption,
211                     metricsIntermediateDir.path
212                 )
213             }
214         }
215         if (enableReports) {
216             project.rootProject.tasks.named(zipComposeReportsTaskName).configure { zipTask ->
217                 zipTask.dependsOn(compileTasks)
218             }
219 
220             val reportsIntermediateDir = project.compilerReportsIntermediatesDir()
221             compileTasks.configureEach { compile ->
222                 compile.addPluginOption(
223                     ComposeCompileOptions.ReportsOption,
224                     reportsIntermediateDir.path
225                 )
226             }
227         }
228     }
229 }
230 
KotlinCompilenull231 private fun KotlinCompile.addPluginOption(
232     composeCompileOptions: ComposeCompileOptions,
233     value: String
234 ) =
235     pluginOptions.add(
236         CompilerPluginConfig().apply {
237             addPluginArgument(
238                 composeCompileOptions.pluginId,
239                 SubpluginOption(composeCompileOptions.key, value)
240             )
241         }
242     )
243 
KotlinCompilenull244 private fun KotlinCompile.enableFeatureFlag(featureFlag: ComposeFeatureFlag) {
245     addPluginOption(ComposeCompileOptions.FeatureFlagOption, featureFlag.featureName)
246 }
247 
KotlinCompilenull248 private fun KotlinCompile.disableFeatureFlag(featureFlag: ComposeFeatureFlag) {
249     addPluginOption(ComposeCompileOptions.FeatureFlagOption, "-${featureFlag.featureName}")
250 }
251 
zipComposeCompilerMetricsnull252 internal fun Project.zipComposeCompilerMetrics() {
253     if (project.enableComposeCompilerMetrics()) {
254         val zipComposeMetrics =
255             project.tasks.register(zipComposeMetricsTaskName, Zip::class.java) { zipTask ->
256                 zipTask.from(project.compilerMetricsIntermediatesDir())
257                 zipTask.destinationDirectory.set(project.composeCompilerDataDir())
258                 zipTask.archiveBaseName.set("composemetrics")
259             }
260         project.addToBuildOnServer(zipComposeMetrics)
261     }
262 }
263 
zipComposeCompilerReportsnull264 internal fun Project.zipComposeCompilerReports() {
265     if (project.enableComposeCompilerReports()) {
266         val zipComposeReports =
267             project.tasks.register(zipComposeReportsTaskName, Zip::class.java) { zipTask ->
268                 zipTask.from(project.compilerReportsIntermediatesDir())
269                 zipTask.destinationDirectory.set(project.composeCompilerDataDir())
270                 zipTask.archiveBaseName.set("composereports")
271             }
272         project.addToBuildOnServer(zipComposeReports)
273     }
274 }
275 
Projectnull276 private fun Project.compilerMetricsIntermediatesDir(): File {
277     return project.rootProject.layout.buildDirectory
278         .dir("libraryreports/composemetrics")
279         .get()
280         .asFile
281 }
282 
Projectnull283 private fun Project.compilerReportsIntermediatesDir(): File {
284     return project.rootProject.layout.buildDirectory
285         .dir("libraryreports/composereports")
286         .get()
287         .asFile
288 }
289 
Projectnull290 private fun Project.composeCompilerDataDir(): File {
291     return File(getDistributionDirectory(), "compose-compiler-data")
292 }
293 
294 private const val ComposePluginId = "androidx.compose.compiler.plugins.kotlin"
295 
296 private enum class ComposeCompileOptions(val pluginId: String, val key: String) {
297     SourceOption(ComposePluginId, "sourceInformation"),
298     StrongSkipping(ComposePluginId, "strongSkipping"),
299     NonSkippingGroupOptimization(ComposePluginId, "nonSkippingGroupOptimization"),
300     MetricsOption(ComposePluginId, "metricsDestination"),
301     ReportsOption(ComposePluginId, "reportsDestination"),
302     FeatureFlagOption(ComposePluginId, "featureFlag"),
303 }
304 
305 private enum class ComposeFeatureFlag(val featureName: String) {
306     StrongSkipping("StrongSkipping"),
307     OptimizeNonSkippingGroups("OptimizeNonSkippingGroups"),
308     PausableComposition("PausableComposition"),
309 }
310