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.kythe
18 
19 import androidx.build.KotlinTarget
20 import androidx.build.OperatingSystem
21 import androidx.build.addToBuildOnServer
22 import androidx.build.checkapi.CompilationInputs
23 import androidx.build.checkapi.MultiplatformCompilationInputs
24 import androidx.build.getCheckoutRoot
25 import androidx.build.getOperatingSystem
26 import androidx.build.getPrebuiltsRoot
27 import androidx.build.multiplatformExtension
28 import java.io.File
29 import java.util.jar.JarOutputStream
30 import java.util.zip.ZipEntry
31 import javax.inject.Inject
32 import org.gradle.api.DefaultTask
33 import org.gradle.api.JavaVersion
34 import org.gradle.api.Project
35 import org.gradle.api.artifacts.Configuration
36 import org.gradle.api.file.ConfigurableFileCollection
37 import org.gradle.api.file.DirectoryProperty
38 import org.gradle.api.file.RegularFileProperty
39 import org.gradle.api.provider.ListProperty
40 import org.gradle.api.provider.Property
41 import org.gradle.api.tasks.CacheableTask
42 import org.gradle.api.tasks.Classpath
43 import org.gradle.api.tasks.Input
44 import org.gradle.api.tasks.InputFile
45 import org.gradle.api.tasks.InputFiles
46 import org.gradle.api.tasks.Internal
47 import org.gradle.api.tasks.OutputDirectory
48 import org.gradle.api.tasks.OutputFile
49 import org.gradle.api.tasks.PathSensitive
50 import org.gradle.api.tasks.PathSensitivity
51 import org.gradle.api.tasks.TaskAction
52 import org.gradle.process.ExecOperations
53 import org.jetbrains.kotlin.gradle.dsl.JvmTarget
54 import org.jetbrains.kotlin.gradle.tasks.KotlinCompilationTask
55 
56 /** Generates kzip files that are used to index the Kotlin source code in Kythe. */
57 @CacheableTask
58 abstract class GenerateKotlinKzipTask
59 @Inject
60 constructor(private val execOperations: ExecOperations) : DefaultTask() {
61 
62     @get:InputFile
63     @get:PathSensitive(PathSensitivity.NONE)
64     abstract val kotlincExtractorBin: RegularFileProperty
65 
66     /** Must be run in the checkout root so as to be free of relative markers */
67     @get:Internal val checkoutRoot: File = project.getCheckoutRoot()
68 
69     @get:Internal val isKmp: Boolean = project.multiplatformExtension != null
70 
71     @get:Input abstract val kotlincFreeCompilerArgs: ListProperty<String>
72 
73     @get:InputFiles
74     @get:PathSensitive(PathSensitivity.RELATIVE)
75     abstract val sourcePaths: ConfigurableFileCollection
76 
77     @get:InputFiles
78     @get:PathSensitive(PathSensitivity.RELATIVE)
79     abstract val commonModuleSourcePaths: ConfigurableFileCollection
80 
81     /** Path to `vnames.json` file, used for name mappings within Kythe. */
82     @get:InputFiles
83     @get:PathSensitive(PathSensitivity.NONE)
84     abstract val vnamesJson: RegularFileProperty
85 
86     @get:Classpath abstract val dependencyClasspath: ConfigurableFileCollection
87 
88     @get:Classpath abstract val compiledSources: ConfigurableFileCollection
89 
90     @get:Input abstract val kotlinTarget: Property<KotlinTarget>
91 
92     @get:Input abstract val jvmTarget: Property<JvmTarget>
93 
94     @get:OutputFile abstract val kzipOutputFile: RegularFileProperty
95 
96     @get:OutputDirectory abstract val kytheClassJarsDir: DirectoryProperty
97 
98     @TaskAction
99     fun exec() {
100         val sourceFiles =
101             sourcePaths.asFileTree.files
102                 .takeIf { files -> files.any { it.extension == "kt" } }
103                 ?.filter { it.extension == "kt" || it.extension == "java" }
104                 ?.map { it.relativeTo(checkoutRoot) }
105                 .orEmpty()
106 
107         if (sourceFiles.isEmpty()) {
108             return
109         }
110 
111         val commonSourceFiles =
112             commonModuleSourcePaths.asFileTree.files
113                 .filter { it.extension == "kt" || it.extension == "java" }
114                 .map { it.relativeTo(checkoutRoot) }
115 
116         val dependencyClasspath =
117             dependencyClasspath.files
118                 .filter { it.exists() }
119                 .mapNotNull { file ->
120                     when {
121                         file.isFile && file.extension == "jar" -> {
122                             file.relativeTo(checkoutRoot)
123                         }
124                         file.isDirectory -> {
125                             file
126                                 .createJarFromDirectory(
127                                     kytheClassJarsDir.get().asFile,
128                                     checkoutRoot
129                                 )
130                                 .relativeTo(checkoutRoot)
131                         }
132                         else -> null
133                     }
134                 }
135 
136         val args = buildList {
137             addAll(
138                 listOf(
139                     // Kythe drops arg[0] as it's unix convention that is the executable name
140                     "kotlinc",
141                     "-jvm-target",
142                     jvmTarget.get().target,
143                     "-no-reflect",
144                     "-no-stdlib",
145                     "-api-version",
146                     kotlinTarget.get().apiVersion.version,
147                     "-language-version",
148                     kotlinTarget.get().apiVersion.version,
149                     "-opt-in=kotlin.contracts.ExperimentalContracts"
150                 )
151             )
152         }
153 
154         val multiplatformArg =
155             if (isKmp) {
156                 listOf("-Xmulti-platform")
157             } else emptyList()
158 
159         val filteredKotlincFreeCompilerArgs =
160             kotlincFreeCompilerArgs.get().distinct().filter { !it.startsWith("-Xjdk-release") }
161 
162         val command = buildList {
163             add(kotlincExtractorBin.get().asFile)
164             addAll(
165                 listOf(
166                     "-corpus",
167                     ANDROIDX_CORPUS,
168                     "-kotlin_out",
169                     compiledSources.singleFile.relativeTo(checkoutRoot).path,
170                     "-o",
171                     kzipOutputFile.get().asFile.relativeTo(checkoutRoot).path,
172                     "-vnames",
173                     vnamesJson.get().asFile.relativeTo(checkoutRoot).path,
174                     "-args",
175                     (args + multiplatformArg + filteredKotlincFreeCompilerArgs).joinToString(" ")
176                 )
177             )
178             sourceFiles.forEach { addAll(listOf("-srcs", it.path)) }
179             commonSourceFiles.forEach { addAll(listOf("-common_srcs", it.path)) }
180             dependencyClasspath.forEach { addAll(listOf("-cp", it.path)) }
181         }
182 
183         execOperations.exec {
184             it.commandLine(command)
185             it.workingDir = checkoutRoot
186         }
187     }
188 
189     internal companion object {
190         fun setupProject(
191             project: Project,
192             compilationInputs: CompilationInputs,
193             compiledSources: Configuration,
194             kotlinTarget: Property<KotlinTarget>,
195             javaVersion: JavaVersion,
196         ) {
197             val kotlincFreeCompilerArgs =
198                 project.objects.listProperty(String::class.java).apply {
199                     project.tasks.withType(KotlinCompilationTask::class.java).configureEach {
200                         addAll(it.compilerOptions.freeCompilerArgs)
201                     }
202                 }
203             project.tasks
204                 .register("generateKotlinKzip", GenerateKotlinKzipTask::class.java) { task ->
205                     task.apply {
206                         kotlincExtractorBin.set(
207                             File(
208                                 project.getPrebuiltsRoot(),
209                                 "build-tools/${osName()}/bin/kotlinc_extractor"
210                             )
211                         )
212                         sourcePaths.setFrom(compilationInputs.sourcePaths)
213                         commonModuleSourcePaths.from(
214                             (compilationInputs as? MultiplatformCompilationInputs)
215                                 ?.commonModuleSourcePaths
216                         )
217                         vnamesJson.set(project.getVnamesJson())
218                         dependencyClasspath.setFrom(
219                             compilationInputs.dependencyClasspath + compilationInputs.bootClasspath
220                         )
221                         this.compiledSources.setFrom(compiledSources)
222                         this.kotlinTarget.set(kotlinTarget)
223                         jvmTarget.set(JvmTarget.fromTarget(javaVersion.toString()))
224                         kzipOutputFile.set(
225                             File(
226                                 project.layout.buildDirectory.get().asFile,
227                                 "kzips/${project.group}-${project.name}.kotlin.kzip"
228                             )
229                         )
230                         kytheClassJarsDir.set(project.layout.buildDirectory.dir("kythe-class-jars"))
231                         this.kotlincFreeCompilerArgs.set(kotlincFreeCompilerArgs)
232                     }
233                 }
234                 .also { project.addToBuildOnServer(it) }
235         }
236     }
237 }
238 
osNamenull239 private fun osName() =
240     when (getOperatingSystem()) {
241         OperatingSystem.LINUX -> "linux-x86"
242         OperatingSystem.MAC -> "darwin-x86"
243         OperatingSystem.WINDOWS -> error("Kzip generation not supported in Windows")
244     }
245 
246 /* Kythe processes only JARs, so we create JARs from directory content. */
createJarFromDirectorynull247 private fun File.createJarFromDirectory(kytheClassJarsDir: File, baseDir: File): File {
248     val jarParentDir = File(kytheClassJarsDir, this.relativeTo(baseDir).invariantSeparatorsPath)
249     jarParentDir.mkdirs()
250 
251     val jarFile = File(jarParentDir, "${this.name}.jar")
252     JarOutputStream(jarFile.outputStream()).use { jarOut ->
253         this.walkTopDown()
254             .filter { it.isFile }
255             .forEach { file ->
256                 val entryName = file.relativeTo(this).invariantSeparatorsPath
257                 jarOut.putNextEntry(ZipEntry(entryName))
258                 file.inputStream().use { it.copyTo(jarOut) }
259                 jarOut.closeEntry()
260             }
261     }
262     return jarFile
263 }
264