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