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