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