Merge "Clang Extension" into androidx-main
diff --git a/buildSrc-tests/src/test/java/androidx/build/clang/AndroidXClangTest.kt b/buildSrc-tests/src/test/java/androidx/build/clang/AndroidXClangTest.kt
new file mode 100644
index 0000000..f9a9dd9
--- /dev/null
+++ b/buildSrc-tests/src/test/java/androidx/build/clang/AndroidXClangTest.kt
@@ -0,0 +1,280 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.build.clang
+
+import androidx.testutils.gradle.ProjectSetupRule
+import com.google.common.truth.Truth.assertThat
+import java.io.File
+import org.gradle.api.Project
+import org.gradle.api.file.ConfigurableFileCollection
+import org.gradle.api.plugins.ExtraPropertiesExtension
+import org.gradle.testfixtures.ProjectBuilder
+import org.jetbrains.kotlin.konan.target.HostManager
+import org.jetbrains.kotlin.konan.target.KonanTarget
+import org.junit.AssumptionViolatedException
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.rules.TemporaryFolder
+
+class AndroidXClangTest {
+ @get:Rule
+ val projectSetup = ProjectSetupRule()
+
+ @get:Rule
+ val tmpFolder = TemporaryFolder()
+ private lateinit var project: Project
+ private lateinit var clangExtension: AndroidXClang
+
+ @Before
+ fun init() {
+ project = ProjectBuilder.builder().withProjectDir(projectSetup.rootDir).build()
+ val extension = project.rootProject.property("ext") as ExtraPropertiesExtension
+ // build service needs prebuilts location to "download" clang and targets.
+ extension.set(
+ "prebuiltsRoot", File(projectSetup.props.rootProjectPath).resolve("../../prebuilts")
+ )
+ clangExtension = AndroidXClang(project)
+ }
+
+ @Test
+ fun addJniHeaders() {
+ val multiTargetNativeCompilation = clangExtension.createNativeCompilation(
+ "mylib"
+ ) {
+ it.configureEachTarget {
+ it.addJniHeaders()
+ }
+ }
+ multiTargetNativeCompilation.configureTargets(
+ listOf(KonanTarget.LINUX_X64, KonanTarget.ANDROID_X64)
+ )
+ // trigger configuration
+ multiTargetNativeCompilation.targetProvider(KonanTarget.LINUX_X64).get()
+ multiTargetNativeCompilation.targetProvider(KonanTarget.ANDROID_X64).get()
+ val compileTasks = project.tasks.withType(ClangCompileTask::class.java).toList()
+ val linuxCompileTask = compileTasks.first {
+ it.clangParameters.konanTarget.get().asKonanTarget == KonanTarget.LINUX_X64
+ }
+ // make sure it includes linux header
+ assertThat(
+ linuxCompileTask.clangParameters.includes.regularFileNames()
+ ).contains("jni.h")
+ val androidCompileTask = compileTasks.first {
+ it.clangParameters.konanTarget.get().asKonanTarget == KonanTarget.ANDROID_X64
+ }
+ // android has jni in sysroots, hence we shouldn't add that
+ assertThat(
+ androidCompileTask.clangParameters.includes.regularFileNames()
+ ).doesNotContain("jni.h")
+ }
+
+ @Test
+ fun configureTargets() {
+ val commonSourceFolders = tmpFolder.newFolder("src").also {
+ it.resolve("src1.c").writeText("")
+ it.resolve("src2.c").writeText("")
+ }
+ val commonIncludeFolders = listOf(
+ tmpFolder.newFolder("include1"),
+ tmpFolder.newFolder("include2"),
+ )
+ val linuxSrcFolder = tmpFolder.newFolder("linuxOnlySrc").also {
+ it.resolve("linuxSrc1.c").writeText("")
+ it.resolve("linuxSrc2.c").writeText("")
+ }
+ val androidIncludeFolders = listOf(
+ tmpFolder.newFolder("androidInclude1"),
+ tmpFolder.newFolder("androidInclude2"),
+ )
+ val multiTargetNativeCompilation = clangExtension.createNativeCompilation(
+ "mylib"
+ ) {
+ it.configureEachTarget {
+ it.sources.from(commonSourceFolders)
+ it.includes.from(commonIncludeFolders)
+ it.freeArgs.addAll("commonArg1", "commonArg2")
+ if (it.konanTarget == KonanTarget.LINUX_X64) {
+ it.freeArgs.addAll("linuxArg1")
+ }
+ if (it.konanTarget == KonanTarget.ANDROID_X64) {
+ it.freeArgs.addAll("androidArg1")
+ }
+ }
+ }
+ multiTargetNativeCompilation.configureTarget(KonanTarget.LINUX_X64) {
+ it.sources.from(linuxSrcFolder)
+ }
+ // multiple configure calls on the target
+ multiTargetNativeCompilation.configureTarget(KonanTarget.LINUX_X64) {
+ it.freeArgs.addAll("linuxArg2")
+ }
+ multiTargetNativeCompilation.configureTarget(KonanTarget.ANDROID_X64) {
+ it.includes.from(androidIncludeFolders)
+ it.freeArgs.addAll("androidArg2")
+ }
+
+ // configurations are done lazily when needed
+ assertThat(project.tasks.withType(
+ ClangCompileTask::class.java
+ ).toList()).isEmpty()
+
+ // trigger configuration of targets
+ multiTargetNativeCompilation.targetProvider(KonanTarget.LINUX_X64).get()
+ multiTargetNativeCompilation.targetProvider(KonanTarget.ANDROID_X64).get()
+
+ // make sure it created tasks for it
+ project.tasks.withType(ClangCompileTask::class.java).let { compileTasks ->
+ // 2 compile tasks, 1 for linux, 1 for android
+ assertThat(compileTasks).hasSize(2)
+ val linuxTask = compileTasks.first {
+ it.clangParameters.konanTarget.get().asKonanTarget == KonanTarget.LINUX_X64
+ }
+ assertThat(
+ linuxTask.clangParameters.sources.regularFileNames()
+ ).containsExactly("src1.c", "src2.c", "linuxSrc1.c", "linuxSrc2.c")
+ assertThat(
+ linuxTask.clangParameters.includes.directoryNames()
+ ).containsExactly("include1", "include2")
+ assertThat(
+ linuxTask.clangParameters.freeArgs.get()
+ ).containsExactly("commonArg1", "commonArg2", "linuxArg1", "linuxArg2")
+
+ val androidTask = compileTasks.first {
+ it.clangParameters.konanTarget.get().asKonanTarget == KonanTarget.ANDROID_X64
+ }
+ assertThat(
+ androidTask.clangParameters.sources.regularFileNames()
+ ).containsExactly("src1.c", "src2.c")
+ assertThat(
+ androidTask.clangParameters.includes.directoryNames()
+ ).containsExactly(
+ "androidInclude1", "androidInclude2", "include1", "include2"
+ )
+ assertThat(
+ androidTask.clangParameters.freeArgs.get()
+ ).containsExactly("commonArg1", "commonArg2", "androidArg1", "androidArg2")
+ }
+ // 2 archive tasks, 1 for each target
+ project.tasks.withType(ClangArchiveTask::class.java).let { archiveTasks ->
+ assertThat(archiveTasks).hasSize(2)
+ assertThat(
+ archiveTasks.map { it.llvmArchiveParameters.konanTarget.get().asKonanTarget }
+ ).containsExactly(
+ KonanTarget.LINUX_X64,
+ KonanTarget.ANDROID_X64
+ )
+ archiveTasks.forEach { archiveTask ->
+ assertThat(
+ archiveTask.llvmArchiveParameters.outputFile.get().asFile.name
+ ).isEqualTo(
+ "libmylib.a"
+ )
+ }
+ }
+
+ // 2 shared library tasks, 1 for each target
+ project.tasks.withType(ClangSharedLibraryTask::class.java).let { soTasks ->
+ assertThat(
+ soTasks.map { it.clangParameters.konanTarget.get().asKonanTarget }
+ ).containsExactly(
+ KonanTarget.LINUX_X64,
+ KonanTarget.ANDROID_X64
+ )
+ soTasks.forEach {
+ assertThat(
+ it.clangParameters.outputFile.get().asFile.name
+ ).isEqualTo("libmylib.so")
+ }
+ }
+ }
+
+ @Test
+ fun configureDisabledTarget() {
+ if (HostManager.hostIsMac) {
+ throw AssumptionViolatedException(
+ """
+ All targets are enabled on mac, hence we cannot end-to-end test disabled targets.
+ """.trimIndent()
+ )
+ }
+ val multiTargetNativeCompilation = clangExtension.createNativeCompilation(
+ "mylib"
+ ) {
+ it.configureEachTarget {
+ it.sources.from(tmpFolder.newFolder())
+ }
+ }
+ multiTargetNativeCompilation.configureTarget(KonanTarget.LINUX_X64)
+ multiTargetNativeCompilation.configureTarget(KonanTarget.MACOS_ARM64)
+ assertThat(multiTargetNativeCompilation.hasTarget(
+ KonanTarget.LINUX_X64
+ )).isTrue()
+ assertThat(multiTargetNativeCompilation.hasTarget(
+ KonanTarget.MACOS_ARM64
+ )).isFalse()
+ }
+
+ @Test
+ fun linking() {
+ val lib1Sources = tmpFolder.newFolder().also {
+ it.resolve("src1.c").writeText("")
+ }
+ val lib2Sources = tmpFolder.newFolder().also {
+ it.resolve("src2.c").writeText("")
+ }
+ val compilation1 = clangExtension.createNativeCompilation(
+ "lib1"
+ ) {
+ it.configureEachTarget {
+ it.sources.from(lib1Sources)
+ }
+ }
+ compilation1.configureTargets(listOf(KonanTarget.LINUX_X64, KonanTarget.ANDROID_X64))
+ val compilation2 = clangExtension.createNativeCompilation(
+ "lib2"
+ ) {
+ it.configureEachTarget {
+ it.sources.from(lib2Sources)
+ it.linkWith(compilation1)
+ }
+ }
+ compilation2.configureTargets(listOf(KonanTarget.LINUX_X64, KonanTarget.ANDROID_X64))
+ // trigger configuration
+ compilation2.targetProvider(KonanTarget.LINUX_X64).get()
+ compilation2.targetProvider(KonanTarget.ANDROID_X64).get()
+ val sharedLibrariesTasks = project.tasks.withType(
+ ClangSharedLibraryTask::class.java
+ ).toList().filter {
+ it.name.contains("lib2", ignoreCase = true)
+ }
+ assertThat(sharedLibrariesTasks).hasSize(2)
+ sharedLibrariesTasks.forEach {
+ assertThat(
+ it.clangParameters.linkedObjects.files.map { it.name }
+ ).containsExactly("liblib1.so")
+ }
+ }
+
+ private fun ConfigurableFileCollection.regularFileNames() = asFileTree.files.map {
+ it.name
+ }
+
+ private fun ConfigurableFileCollection.directoryNames() = files.flatMap {
+ it.walkTopDown()
+ }.filter { it.isDirectory }.map { it.name }
+}
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/clang/AndroidXClang.kt b/buildSrc/private/src/main/kotlin/androidx/build/clang/AndroidXClang.kt
new file mode 100644
index 0000000..bf8d46a
--- /dev/null
+++ b/buildSrc/private/src/main/kotlin/androidx/build/clang/AndroidXClang.kt
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.build.clang
+
+import com.google.common.annotations.VisibleForTesting
+import org.gradle.api.Action
+import org.gradle.api.Project
+
+@VisibleForTesting // need to access it from buildSrc-test
+class AndroidXClang(
+ val project: Project
+) {
+ private val multiTargetNativeCompilations = mutableMapOf<String, MultiTargetNativeCompilation>()
+ fun createNativeCompilation(
+ archiveName: String,
+ configure: Action<MultiTargetNativeCompilation>,
+ ): MultiTargetNativeCompilation {
+ val multiTargetNativeCompilation = multiTargetNativeCompilations.getOrPut(archiveName) {
+ MultiTargetNativeCompilation(
+ project = project, archiveName = archiveName
+ )
+ }
+ configure.execute(multiTargetNativeCompilation)
+ return multiTargetNativeCompilation
+ }
+}
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/clang/MultiTargetNativeCompilation.kt b/buildSrc/private/src/main/kotlin/androidx/build/clang/MultiTargetNativeCompilation.kt
new file mode 100644
index 0000000..3710d36
--- /dev/null
+++ b/buildSrc/private/src/main/kotlin/androidx/build/clang/MultiTargetNativeCompilation.kt
@@ -0,0 +1,273 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.build.clang
+
+import com.android.utils.appendCapitalized
+import org.gradle.api.Action
+import org.gradle.api.NamedDomainObjectFactory
+import org.gradle.api.Project
+import org.gradle.api.file.ConfigurableFileCollection
+import org.gradle.api.file.RegularFile
+import org.gradle.api.provider.ListProperty
+import org.gradle.api.provider.Provider
+import org.gradle.api.tasks.TaskProvider
+import org.gradle.kotlin.dsl.listProperty
+import org.jetbrains.kotlin.konan.target.HostManager
+import org.jetbrains.kotlin.konan.target.KonanTarget
+
+/**
+ * A native compilation setup (C code) that can target multiple platforms.
+ *
+ * New targets can be added via the [configureTarget] method. Each configured target will have
+ * tasks to produce machine code (.o), shared library (.so / .dylib) or archive (.a).
+ *
+ * Common configuration between targets can be done via the [configureEachTarget] method.
+ *
+ * @see NativeTargetCompilation for configuration details for each target.
+ */
+class MultiTargetNativeCompilation(
+ internal val project: Project,
+ internal val archiveName: String,
+) {
+ private val hostManager = HostManager()
+
+ private val nativeTargets =
+ project.objects.domainObjectContainer(
+ NativeTargetCompilation::class.java,
+ Factory(
+ project,
+ archiveName
+ )
+ )
+
+ /**
+ * Returns true if native code targeting [konanTarget] can be compiled on this host machine.
+ */
+ fun canCompileOnCurrentHost(konanTarget: KonanTarget) = hostManager.isEnabled(konanTarget)
+
+ /**
+ * Calls the given [action] for each added [KonanTarget] in this compilation.
+ */
+ @Suppress("unused") // used in build.gradle
+ fun configureEachTarget(
+ action: Action<NativeTargetCompilation>
+ ) {
+ nativeTargets.configureEach {
+ action.execute(it)
+ }
+ }
+
+ /**
+ * Returns a [RegularFile] provider that points to the shared library output for the given
+ * [konanTarget].
+ */
+ internal fun sharedObjectOutputFor(
+ konanTarget: KonanTarget
+ ): Provider<RegularFile> {
+ return nativeTargets.named(konanTarget.name).flatMap { nativeTargetCompilation ->
+ nativeTargetCompilation.sharedLibTask.flatMap { it.clangParameters.outputFile }
+ }
+ }
+
+ /**
+ * Adds the given [konanTarget] to the list of compilation target if it can be built on this
+ * machine. The [action] block can be used to further configure the parameters of that
+ * compilation.
+ */
+ @Suppress("MemberVisibilityCanBePrivate") // used in build.gradle
+ @JvmOverloads
+ fun configureTarget(
+ konanTarget: KonanTarget,
+ action: Action<NativeTargetCompilation>? = null
+ ) {
+ if (!canCompileOnCurrentHost(konanTarget)) {
+ // Cannot compile it on this host. This is similar to calling `ios` block in the build
+ // gradle file on a linux machine.
+ return
+ }
+ val nativeTarget = if (nativeTargets.names.contains(konanTarget.name)) {
+ nativeTargets.named(konanTarget.name)
+ } else {
+ nativeTargets.register(konanTarget.name)
+ }
+ if (action != null) {
+ nativeTarget.configure(action)
+ }
+ }
+
+ /**
+ * Returns a provider for the given konan target and throws an exception if it is not
+ * registered.
+ */
+ fun targetProvider(konanTarget: KonanTarget) = nativeTargets.named(konanTarget.name)
+
+ /**
+ * Returns true if the given [konanTarget] is configured as a compilation target.
+ */
+ fun hasTarget(konanTarget: KonanTarget) =
+ nativeTargets.names.contains(konanTarget.name)
+
+ /**
+ * Convenience method to configure multiple targets at the same time.
+ * This is equal to calling [configureTarget] for each given [konanTargets].
+ */
+ @Suppress("unused") // used in build.gradle
+ fun configureTargets(
+ konanTargets: List<KonanTarget>,
+ action: Action<NativeTargetCompilation>? = null
+ ) = konanTargets.map { configureTarget(it, action) }
+
+ /**
+ * Internal factory for creating instances of [nativeTargets]. This factory sets up all
+ * necessary inputs and their tasks for the native target.
+ */
+ private class Factory(
+ private val project: Project,
+ private val archiveName: String,
+ ) : NamedDomainObjectFactory<NativeTargetCompilation> {
+ /**
+ * Shared task prefix for this archive
+ */
+ private val taskPrefix = "nativeCompilationFor".appendCapitalized(archiveName)
+
+ /**
+ * Shared output directory prefix for tasks of this archive.
+ */
+ private val outputDir = project.layout.buildDirectory.dir(
+ "clang".appendCapitalized(archiveName)
+ )
+
+ override fun create(name: String): NativeTargetCompilation {
+ return create(SerializableKonanTarget(name))
+ }
+
+ @JvmName("createWithSerializableKonanTarget")
+ private fun create(
+ serializableKonanTarget: SerializableKonanTarget
+ ): NativeTargetCompilation {
+ val includes = project.objects.fileCollection()
+ val sources = project.objects.fileCollection()
+ val freeArgs = project.objects.listProperty<String>()
+ val linkedObjects = project.objects.fileCollection()
+ val compileTask =
+ createCompileTask(serializableKonanTarget, includes, sources, freeArgs)
+ val archiveTask = createArchiveTask(serializableKonanTarget, compileTask)
+ val sharedLibTask =
+ createSharedLibraryTask(serializableKonanTarget, compileTask, linkedObjects)
+ return NativeTargetCompilation(
+ project = project,
+ konanTarget = serializableKonanTarget.asKonanTarget,
+ compileTask = compileTask,
+ archiveTask = archiveTask,
+ sharedLibTask = sharedLibTask,
+ sources = sources,
+ includes = includes,
+ linkedObjects = linkedObjects,
+ freeArgs = freeArgs
+ )
+ }
+
+ private fun createArchiveTask(
+ serializableKonanTarget: SerializableKonanTarget,
+ compileTask: TaskProvider<ClangCompileTask>
+ ): TaskProvider<ClangArchiveTask> {
+ val archiveTaskName = taskPrefix.appendCapitalized(
+ "archive",
+ serializableKonanTarget.name
+ )
+ val archiveTask = project.tasks.register(
+ archiveTaskName, ClangArchiveTask::class.java
+ ) { task ->
+ val konanTarget = serializableKonanTarget.asKonanTarget
+ val archiveFileName = listOf(
+ konanTarget.family.staticPrefix,
+ archiveName,
+ ".",
+ konanTarget.family.staticSuffix
+ ).joinToString("")
+ task.usesService(KonanBuildService.obtain(project))
+ task.llvmArchiveParameters.let { llvmAr ->
+ llvmAr.outputFile.set(outputDir.map {
+ it.file("$serializableKonanTarget/$archiveFileName")
+ })
+ llvmAr.konanTarget.set(serializableKonanTarget)
+ llvmAr.objectFiles.from(compileTask.map { it.clangParameters.output })
+ }
+ }
+ return archiveTask
+ }
+
+ private fun createCompileTask(
+ serializableKonanTarget: SerializableKonanTarget,
+ includes: ConfigurableFileCollection?,
+ sources: ConfigurableFileCollection?,
+ freeArgs: ListProperty<String>
+ ): TaskProvider<ClangCompileTask> {
+ val compileTaskName = taskPrefix.appendCapitalized(
+ "compile",
+ serializableKonanTarget.name
+ )
+ val compileTask = project.tasks.register(
+ compileTaskName, ClangCompileTask::class.java
+ ) { compileTask ->
+ compileTask.usesService(KonanBuildService.obtain(project))
+ compileTask.clangParameters.let { clang ->
+ clang.output.set(outputDir.map { it.dir("compile/$serializableKonanTarget") })
+ clang.includes.from(includes)
+ clang.sources.from(sources)
+ clang.freeArgs.addAll(freeArgs)
+ clang.konanTarget.set(serializableKonanTarget)
+ }
+ }
+ return compileTask
+ }
+
+ private fun createSharedLibraryTask(
+ serializableKonanTarget: SerializableKonanTarget,
+ compileTask: TaskProvider<ClangCompileTask>,
+ linkedObjects: ConfigurableFileCollection,
+ ): TaskProvider<ClangSharedLibraryTask> {
+ val archiveTaskName =
+ taskPrefix.appendCapitalized(
+ "createSharedLibrary",
+ serializableKonanTarget.name
+ )
+ val archiveTask = project.tasks.register(
+ archiveTaskName, ClangSharedLibraryTask::class.java
+ ) { task ->
+ val konanTarget = serializableKonanTarget.asKonanTarget
+ val archiveFileName = listOf(
+ konanTarget.family.staticPrefix,
+ archiveName,
+ ".",
+ konanTarget.family.dynamicSuffix
+ ).joinToString("")
+
+ task.usesService(KonanBuildService.obtain(project))
+ task.clangParameters.let { clang ->
+ clang.outputFile.set(outputDir.map {
+ it.file("$serializableKonanTarget/$archiveFileName")
+ })
+ clang.konanTarget.set(serializableKonanTarget)
+ clang.objectFiles.from(compileTask.map { it.clangParameters.output })
+ clang.linkedObjects.from(linkedObjects)
+ }
+ }
+ return archiveTask
+ }
+ }
+}
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/clang/NativeTargetCompilation.kt b/buildSrc/private/src/main/kotlin/androidx/build/clang/NativeTargetCompilation.kt
new file mode 100644
index 0000000..4031962
--- /dev/null
+++ b/buildSrc/private/src/main/kotlin/androidx/build/clang/NativeTargetCompilation.kt
@@ -0,0 +1,129 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.build.clang
+
+import java.io.File
+import org.gradle.api.Named
+import org.gradle.api.Project
+import org.gradle.api.file.ConfigurableFileCollection
+import org.gradle.api.provider.ListProperty
+import org.gradle.api.tasks.TaskProvider
+import org.jetbrains.kotlin.konan.target.Family
+import org.jetbrains.kotlin.konan.target.KonanTarget
+
+/**
+ * Represents a C compilation for a single [konanTarget].
+ *
+ * @param konanTarget Target host for the compilation.
+ * @param compileTask The task that compiles the sources and build .o file for each source file.
+ * @param archiveTask The task that will archive the output of the [compileTask] into a single .a
+ * file.
+ * @param sharedLibTask The task that will created a shared library from the output of [compileTask]
+ * that also optionally links with [linkedObjects]
+ * @param sources List of source files for the compilation.
+ * @param includes List of include directories containing .h files for the compilation.
+ * @param linkedObjects List of object files that should be dynamically linked in the final shared
+ * object output.
+ * @param freeArgs Arguments that will be passed into clang for compilation.
+ */
+class NativeTargetCompilation internal constructor(
+ val project: Project,
+ val konanTarget: KonanTarget,
+ internal val compileTask: TaskProvider<ClangCompileTask>,
+ internal val archiveTask: TaskProvider<ClangArchiveTask>,
+ internal val sharedLibTask: TaskProvider<ClangSharedLibraryTask>,
+ val sources: ConfigurableFileCollection,
+ val includes: ConfigurableFileCollection,
+ val linkedObjects: ConfigurableFileCollection,
+ @Suppress("unused") // used via build.gradle
+ val freeArgs: ListProperty<String>
+) : Named {
+ override fun getName(): String = konanTarget.name
+
+ /**
+ * Dynamically links the shared library output of this target with the given [dependency]'s
+ * shared library output.
+ */
+ @Suppress("unused") // used from build.gradle
+ fun linkWith(dependency: MultiTargetNativeCompilation) {
+ linkedObjects.from(
+ dependency.sharedObjectOutputFor(konanTarget)
+ )
+ }
+
+ /**
+ * Convenience method to add jni headers to the compilation.
+ */
+ @Suppress("unused") // used from build.gradle
+ fun addJniHeaders() {
+ if (konanTarget.family == Family.ANDROID) {
+ // android already has JNI
+ return
+ }
+
+ includes.from(project.provider {
+ findJniHeaderDirectories()
+ })
+ }
+
+ private fun findJniHeaderDirectories(): List<File> {
+ // TODO b/306669673 add support for GitHub builds.
+ // we need to find 2 jni header files
+ // jni.h -> This is the same across all platforms
+ // jni_md.h -> Includes machine dependant definitions.
+ // Internal Devs: You can read more about it here: http://go/androidx-jni-cross-compilation
+ val javaHome = File(System.getProperty("java.home"))
+
+ // for jni_md, we need to find the prebuilts because each jdk ships with jni_md only for
+ // its own target family.
+ val jdkPrebuiltsRoot = javaHome.parentFile
+
+ val relativeHeaderPaths = when (konanTarget.family) {
+ Family.MINGW -> {
+ listOf(
+ "/windows-x86/include",
+ "/windows-x86/include/win32"
+ )
+ }
+
+ Family.OSX -> {
+ // it is OK that we are using x86 here, they are the same files (openjdk only
+ // distinguishes between unix and windows).
+ listOf(
+ "darwin-x86/include",
+ "darwin-x86/include/darwin"
+ )
+ }
+
+ Family.LINUX -> {
+ listOf(
+ "linux-x86/include",
+ "linux-x86/include/linux",
+ )
+ }
+
+ else -> error("unsupported family ($konanTarget) for JNI compilation")
+ }
+ return relativeHeaderPaths.map {
+ jdkPrebuiltsRoot.resolve(it)
+ }.onEach {
+ check(it.exists()) {
+ "Cannot find header directory (${it.name}) in ${it.canonicalPath}"
+ }
+ }
+ }
+}