1 /*
<lambda>null2  * Copyright 2024 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.sources
18 
19 import androidx.build.addToBuildOnServer
20 import androidx.build.addToCheckTask
21 import androidx.build.multiplatformExtension
22 import androidx.build.uptodatedness.cacheEvenIfNoOutputs
23 import java.io.File
24 import org.gradle.api.DefaultTask
25 import org.gradle.api.GradleException
26 import org.gradle.api.Project
27 import org.gradle.api.file.FileCollection
28 import org.gradle.api.provider.Property
29 import org.gradle.api.tasks.Input
30 import org.gradle.api.tasks.InputFiles
31 import org.gradle.api.tasks.PathSensitive
32 import org.gradle.api.tasks.PathSensitivity
33 import org.gradle.api.tasks.TaskAction
34 import org.gradle.api.tasks.options.Option
35 import org.gradle.work.DisableCachingByDefault
36 import org.jetbrains.kotlin.gradle.plugin.KotlinPlatformType
37 import org.jetbrains.kotlin.gradle.plugin.KotlinTarget
38 
39 @DisableCachingByDefault(because = "Doesn't benefit from caching")
40 abstract class ValidateMultiplatformSourceSetNaming : DefaultTask() {
41 
42     @get:Input abstract val rootDir: Property<String>
43 
44     @InputFiles
45     @PathSensitive(PathSensitivity.RELATIVE)
46     fun getInputFiles(): Collection<FileCollection> = sourceSetMap.values
47 
48     private val sourceSetMap: MutableMap<String, FileCollection> = mutableMapOf()
49 
50     @set:Option(
51         option = "autoFix",
52         description = "Whether to automatically rename files instead of throwing an exception",
53     )
54     @get:Input
55     var autoFix: Boolean = false
56 
57     @TaskAction
58     fun validate() {
59         // Files or entire source sets may duplicated shared across compilations, but it's more
60         // expensive to de-dupe them than to check the suffixes for everything multiple times.
61         for ((sourceFileSuffix, kotlinSourceSet) in sourceSetMap) {
62             for (fileOrDir in kotlinSourceSet) {
63                 for (file in fileOrDir.walk()) {
64                     // Kotlin source files must be uniquely-named across platforms.
65                     if (
66                         file.isFile &&
67                             file.name.endsWith(".kt") &&
68                             !file.name.endsWith(".$sourceFileSuffix.kt")
69                     ) {
70                         val actualPath = file.toRelativeString(File(rootDir.get()))
71                         val expectedName = "${file.name.substringBefore('.')}.$sourceFileSuffix.kt"
72                         if (autoFix) {
73                             val destFile = File(file.parentFile, expectedName)
74                             file.renameTo(destFile)
75                             logger.info("Applied fix: $actualPath -> $expectedName")
76                         } else {
77                             throw GradleException(
78                                 "Source files for non-common platforms must be suffixed with " +
79                                     "their target platform. Found '$actualPath' but expected " +
80                                     "'$expectedName'."
81                             )
82                         }
83                     }
84                 }
85             }
86         }
87     }
88 
89     fun addTarget(project: Project, target: KotlinTarget) {
90         sourceSetMap[target.preferredSourceFileSuffix] =
91             project.files(
92                 target.compilations
93                     .filterNot { compilation ->
94                         // Don't enforce suffixes for test source sets. Names can be e.g. testOnJvm
95                         compilation.name.startsWith("test") || compilation.name.endsWith("Test")
96                     }
97                     .flatMap { compilation -> compilation.kotlinSourceSets }
98                     .map { kotlinSourceSet -> kotlinSourceSet.kotlin.sourceDirectories }
99                     .toTypedArray()
100             )
101     }
102 
103     /**
104      * List of Kotlin target names which may be used as source file suffixes. Any target whose name
105      * does not appear in this list will use its [KotlinPlatformType] name.
106      */
107     private val allowedTargetNameSuffixes =
108         setOf("android", "desktop", "jvm", "commonStubs", "jvmStubs", "linuxx64Stubs", "wasmJs")
109 
110     /** The preferred source file suffix for the target's platform type. */
111     private val KotlinTarget.preferredSourceFileSuffix: String
112         get() =
113             if (allowedTargetNameSuffixes.contains(name)) {
114                 name
115             } else {
116                 platformType.name
117             }
118 }
119 
120 /**
121  * Ensures that multiplatform sources are suffixed with their target platform, ex. `MyClass.jvm.kt`.
122  *
123  * Must be called in afterEvaluate().
124  */
Projectnull125 fun Project.registerValidateMultiplatformSourceSetNamingTask() {
126     val targets = multiplatformExtension?.targets?.filterNot { target -> target.name == "metadata" }
127     if (targets == null || targets.size <= 1) {
128         // We only care about multiplatform projects with more than one target platform.
129         return
130     }
131 
132     tasks
133         .register(
134             "validateMultiplatformSourceSetNaming",
135             ValidateMultiplatformSourceSetNaming::class.java
136         ) { task ->
137             targets
138                 .filterNot { target -> target.platformType.name == "common" }
139                 .forEach { target -> task.addTarget(project, target) }
140             task.rootDir.set(rootDir.path)
141             task.cacheEvenIfNoOutputs()
142         }
143         .also { validateTask ->
144             // Multiplatform projects with no enabled platforms do not actually apply the Kotlin
145             // plugin
146             // and therefore do not have the check task. They are skipped unless a platform is
147             // enabled.
148             if (project.tasks.findByName("check") != null) {
149                 project.addToCheckTask(validateTask)
150                 project.addToBuildOnServer(validateTask)
151             }
152         }
153 }
154