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}"
+            }
+        }
+    }
+}