1 /*
<lambda>null2  * Copyright 2023 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.clang
18 
19 import androidx.build.KonanPrebuiltsSetup
20 import androidx.build.ProjectLayoutType
21 import androidx.build.clang.KonanBuildService.Companion.obtain
22 import androidx.build.getKonanPrebuiltsFolder
23 import java.io.ByteArrayOutputStream
24 import javax.inject.Inject
25 import org.gradle.api.GradleException
26 import org.gradle.api.Project
27 import org.gradle.api.file.DirectoryProperty
28 import org.gradle.api.file.FileCollection
29 import org.gradle.api.provider.Property
30 import org.gradle.api.provider.Provider
31 import org.gradle.api.services.BuildService
32 import org.gradle.api.services.BuildServiceParameters
33 import org.gradle.api.tasks.Optional
34 import org.gradle.process.ExecOperations
35 import org.gradle.process.ExecSpec
36 import org.jetbrains.kotlin.gradle.plugin.KotlinMultiplatformPluginWrapper
37 import org.jetbrains.kotlin.gradle.utils.NativeCompilerDownloader
38 import org.jetbrains.kotlin.konan.TempFiles
39 import org.jetbrains.kotlin.konan.target.Family
40 import org.jetbrains.kotlin.konan.target.LinkerArguments
41 import org.jetbrains.kotlin.konan.target.LinkerOutputKind
42 import org.jetbrains.kotlin.konan.target.Platform
43 import org.jetbrains.kotlin.konan.target.PlatformManager
44 
45 /**
46  * A Gradle BuildService that provides access to Konan Compiler (clang, linker, ar etc) to build
47  * native sources for multiple targets.
48  *
49  * You can obtain the instance via [obtain].
50  *
51  * @see ClangArchiveTask
52  * @see ClangCompileTask
53  * @see ClangSharedLibraryTask
54  */
55 abstract class KonanBuildService @Inject constructor(private val execOperations: ExecOperations) :
56     BuildService<KonanBuildService.Parameters> {
57     private val dist by lazy {
58         // double check that we don't initialize konan distribution without prebuilts in AOSP
59         check(
60             parameters.projectLayoutType.get() == ProjectLayoutType.PLAYGROUND ||
61                 parameters.prebuilts.isPresent
62         ) {
63             """
64                 Prebuilts directory for Konan must be provided when the project is not a playground
65                 project.
66             """
67                 .trimIndent()
68         }
69         KonanPrebuiltsSetup.createKonanDistribution(
70             prebuiltsDirectory = parameters.prebuilts.orNull?.asFile,
71             konanHome = parameters.konanHome.get().asFile
72         )
73     }
74 
75     private val platformManager by lazy { PlatformManager(distribution = dist) }
76 
77     /** @see ClangCompileTask */
78     fun compile(parameters: ClangCompileParameters) {
79         val outputDir = parameters.output.get().asFile
80         outputDir.deleteRecursively()
81         outputDir.mkdirs()
82 
83         val platform = getPlatform(parameters.konanTarget)
84         val additionalArgs = buildList {
85             addAll(parameters.freeArgs.get())
86             add("--compile")
87             parameters.includes.files.forEach { includeDirectory ->
88                 check(includeDirectory.isDirectory) {
89                     "Include parameter for clang must be a directory: $includeDirectory"
90                 }
91                 add("-I${includeDirectory.canonicalPath}")
92             }
93             addAll(parameters.sources.regularFilePaths())
94         }
95 
96         val clangCommand = platform.clang.clangC(*additionalArgs.toTypedArray())
97         execOperations.executeSilently { execSpec ->
98             execSpec.executable = clangCommand.first()
99             execSpec.args(clangCommand.drop(1))
100             execSpec.workingDir = parameters.output.get().asFile
101         }
102     }
103 
104     /** @see ClangArchiveTask */
105     fun archiveLibrary(parameters: ClangArchiveParameters) {
106         val outputFile = parameters.outputFile.get().asFile
107         outputFile.delete()
108         outputFile.parentFile.mkdirs()
109 
110         val platform = getPlatform(parameters.konanTarget)
111         val llvmArgs = buildList {
112             add("rc")
113             add(parameters.outputFile.get().asFile.canonicalPath)
114             addAll(parameters.objectFiles.regularFilePaths())
115         }
116         val commands = platform.clang.llvmAr(*llvmArgs.toTypedArray())
117         execOperations.executeSilently { execSpec ->
118             execSpec.executable = commands.first()
119             execSpec.args(commands.drop(1))
120         }
121     }
122 
123     /** @see ClangSharedLibraryTask */
124     fun createSharedLibrary(parameters: ClangSharedLibraryParameters) {
125         val outputFile = parameters.outputFile.get().asFile
126         outputFile.delete()
127         outputFile.parentFile.mkdirs()
128 
129         val platform = getPlatform(parameters.konanTarget)
130 
131         // Specify max-page-size to align ELF regions to 16kb
132         val linkerFlags =
133             parameters.linkerArgs.get() +
134                 if (parameters.konanTarget.get().asKonanTarget.family == Family.ANDROID) {
135                     listOf("-z", "max-page-size=16384")
136                 } else {
137                     emptyList()
138                 }
139 
140         val objectFiles = parameters.objectFiles.regularFilePaths()
141         val linkedObjectFiles = parameters.linkedObjects.regularFilePaths()
142         val linkCommands =
143             with(platform.linker) {
144                 LinkerArguments(
145                         TempFiles(),
146                         objectFiles = objectFiles,
147                         executable = outputFile.canonicalPath,
148                         libraries = linkedObjectFiles,
149                         linkerArgs = linkerFlags,
150                         optimize = true,
151                         debug = false,
152                         kind = LinkerOutputKind.DYNAMIC_LIBRARY,
153                         outputDsymBundle = "unused",
154                         mimallocEnabled = false,
155                         sanitizer = null
156                     )
157                     .finalLinkCommands()
158             }
159         linkCommands
160             .map { it.argsWithExecutable }
161             .forEach { args ->
162                 execOperations.executeSilently { execSpec ->
163                     execSpec.executable = args.first()
164                     args
165                         .drop(1)
166                         .filterNot {
167                             // TODO b/305804211 Figure out if we would rather pass all args manually
168                             // We use the linker that konan uses to be as similar as possible but
169                             // that
170                             // linker also has konan demangling, which we don't need and not even
171                             // available
172                             // in the default distribution. Hence we remove that parameters.
173                             // In the future, we can consider not using the `platform.linker` but
174                             // then
175                             // we would need to parse the konan.properties file to get the relevant
176                             // necessary parameters like sysroot etc.
177                             // https://github.com/JetBrains/kotlin/blob/master/kotlin-native/build-tools/src/main/kotlin/org/jetbrains/kotlin/KotlinNativeTest.kt#L536
178                             it.contains("--defsym") || it.contains("Konan_cxa_demangle")
179                         }
180                         .forEach { execSpec.args(it) }
181                 }
182             }
183     }
184 
185     private fun FileCollection.regularFilePaths(): List<String> {
186         return files
187             .flatMap { it.walkTopDown().filter { it.isFile }.map { it.canonicalPath } }
188             .distinct()
189     }
190 
191     private fun getPlatform(serializableKonanTarget: Property<SerializableKonanTarget>): Platform {
192         val konanTarget = serializableKonanTarget.get().asKonanTarget
193         check(platformManager.enabled.contains(konanTarget)) {
194             "cannot find enabled target with name ${serializableKonanTarget.get()}"
195         }
196         val platform = platformManager.platform(konanTarget)
197         platform.downloadDependencies()
198         return platform
199     }
200 
201     /** Execute the command without logs unless it fails. */
202     private fun <T> ExecOperations.executeSilently(block: (ExecSpec) -> T) {
203         val outputStream = ByteArrayOutputStream()
204         val errorStream = ByteArrayOutputStream()
205         val execResult = exec {
206             block(it)
207             it.setErrorOutput(errorStream)
208             it.setStandardOutput(outputStream)
209             it.isIgnoreExitValue = true // we'll check it below
210         }
211         if (execResult.exitValue != 0) {
212             throw GradleException(
213                 """
214                 Compilation failed:
215                 ==== output:
216                 ${outputStream.toString(Charsets.UTF_8)}
217                 ==== error:
218                 ${errorStream.toString(Charsets.UTF_8)}
219             """
220                     .trimIndent()
221             )
222         }
223     }
224 
225     interface Parameters : BuildServiceParameters {
226         /** KONAN_HOME parameter for initializing konan */
227         val konanHome: DirectoryProperty
228 
229         /** Location if konan prebuilts. Can be null if this is a playground project */
230         @get:Optional val prebuilts: DirectoryProperty
231 
232         /**
233          * The type of the project (Playground vs AOSP main). This value is used to ensure we
234          * initialize Konan distribution properly.
235          */
236         val projectLayoutType: Property<ProjectLayoutType>
237     }
238 
239     companion object {
240         internal const val KEY = "konanBuildService"
241 
242         fun obtain(project: Project): Provider<KonanBuildService> {
243             return project.gradle.sharedServices.registerIfAbsent(
244                 KEY,
245                 KonanBuildService::class.java
246             ) {
247                 check(project.plugins.hasPlugin(KotlinMultiplatformPluginWrapper::class.java)) {
248                     "KonanBuildService can only be used in projects that applied the KMP plugin"
249                 }
250                 check(KonanPrebuiltsSetup.isConfigured(project)) {
251                     "Konan prebuilt directories are not configured for project \"${project.path}\""
252                 }
253                 val nativeCompilerDownloader = NativeCompilerDownloader(project)
254                 nativeCompilerDownloader.downloadIfNeeded()
255 
256                 it.parameters.konanHome.set(nativeCompilerDownloader.compilerDirectory)
257                 it.parameters.projectLayoutType.set(ProjectLayoutType.from(project))
258                 if (!ProjectLayoutType.isPlayground(project)) {
259                     it.parameters.prebuilts.set(project.getKonanPrebuiltsFolder())
260                 }
261             }
262         }
263     }
264 }
265