From 7a1f4304d0c8d18428a6105cfce8898e525ecbe4 Mon Sep 17 00:00:00 2001 From: Sebastian Sellmair Date: Wed, 14 Sep 2022 11:36:14 +0200 Subject: [PATCH] [Gradle][MPP] Implement CompositeMetadataArtifact ^KT-48135 WIP --- .../plugin/mpp/CompositeMetadataArtifact.kt | 77 +++++ .../mpp/CompositeMetadataArtifactImpl.kt | 253 ++++++++++++++ .../gradle/CompositeMetadataArtifactTest.kt | 320 ++++++++++++++++++ 3 files changed, 650 insertions(+) create mode 100644 libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/mpp/CompositeMetadataArtifact.kt create mode 100644 libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/mpp/CompositeMetadataArtifactImpl.kt create mode 100644 libraries/tools/kotlin-gradle-plugin/src/functionalTest/kotlin/org/jetbrains/kotlin/gradle/CompositeMetadataArtifactTest.kt diff --git a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/mpp/CompositeMetadataArtifact.kt b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/mpp/CompositeMetadataArtifact.kt new file mode 100644 index 00000000000..62c5deea476 --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/mpp/CompositeMetadataArtifact.kt @@ -0,0 +1,77 @@ +/* + * Copyright 2010-2022 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. + */ + +package org.jetbrains.kotlin.gradle.plugin.mpp + +import java.io.Closeable +import java.io.File + +internal interface CompositeMetadataArtifact { + + interface Library { + val artifactHandle: ArtifactHandle + val sourceSet: SourceSet + val archiveExtension: String + + /** + * The proposed file-output path. + * This can actually include several path parts if the [Library] requires additional scoping (e.g. [CInteropMetadataLibrary]s + * need to be put into another folder) + */ + val relativeFile: File + + /** + * Copies the content of this [Library] directly into the given [file]. + * The [file] will be overwritten when it already exists. + * Parent directories will be created if necessary. + */ + fun copyTo(file: File): Boolean + + /** + * Copies the content of this [Library] into the [directory] appending the [relativeFile] to it. + * @see copyTo + */ + fun copyIntoDirectory(directory: File) = copyTo(directory.resolve(relativeFile)) + } + + /** + * Represents a Kotlin Metadata library produced by compiling the contained [sourceSet] + */ + interface MetadataLibrary : Library + + /** + * Represents a CInterop Metadata library, produced by the commonizer and attached to the [sourceSet] + */ + interface CInteropMetadataLibrary : Library { + val cinteropLibraryName: String + } + + /** + * Represents a SourceSet packaged into the [CompositeMetadataArtifact] + */ + interface SourceSet { + val artifactHandle: ArtifactHandle + val sourceSetName: String + val metadataLibrary: MetadataLibrary? + val cinteropMetadataLibraries: List + } + + interface ArtifactHandle : Closeable { + val moduleDependencyIdentifier: ModuleDependencyIdentifier + val moduleDependencyVersion: String + val sourceSets: List + fun getSourceSet(name: String): SourceSet + fun findSourceSet(name: String): SourceSet? + } + + val moduleDependencyIdentifier: ModuleDependencyIdentifier + val moduleDependencyVersion: String + + fun open(): ArtifactHandle + + fun read(action: (artifactHandle: ArtifactHandle) -> T): T { + return open().use(action) + } +} diff --git a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/mpp/CompositeMetadataArtifactImpl.kt b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/mpp/CompositeMetadataArtifactImpl.kt new file mode 100644 index 00000000000..fccd8826bb1 --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/mpp/CompositeMetadataArtifactImpl.kt @@ -0,0 +1,253 @@ +/* + * Copyright 2010-2022 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. + */ + +package org.jetbrains.kotlin.gradle.plugin.mpp + +import org.jetbrains.kotlin.gradle.utils.copyPartially +import org.jetbrains.kotlin.gradle.utils.ensureValidZipDirectoryPath +import org.jetbrains.kotlin.gradle.utils.listDescendants +import java.io.Closeable +import java.io.File +import java.io.IOException +import java.util.zip.ZipEntry +import java.util.zip.ZipFile + +internal class CompositeMetadataArtifactImpl( + override val moduleDependencyIdentifier: ModuleDependencyIdentifier, + override val moduleDependencyVersion: String, + private val kotlinProjectStructureMetadata: KotlinProjectStructureMetadata, + private val primaryArtifactFile: File, + private val hostSpecificArtifactFilesBySourceSetName: Map +) : CompositeMetadataArtifact { + + override fun open(): CompositeMetadataArtifact.ArtifactHandle { + return HandlerImpl() + } + + inner class HandlerImpl : CompositeMetadataArtifact.ArtifactHandle { + + override val moduleDependencyIdentifier: ModuleDependencyIdentifier + get() = this@CompositeMetadataArtifactImpl.moduleDependencyIdentifier + + override val moduleDependencyVersion: String + get() = this@CompositeMetadataArtifactImpl.moduleDependencyVersion + + /* Creating SourceSet instances eagerly, as they will only lazily access files */ + private val sourceSetsImpl = kotlinProjectStructureMetadata.sourceSetNames.associateWith { sourceSetName -> + SourceSetImpl(this, sourceSetName, ArtifactFile(hostSpecificArtifactFilesBySourceSetName[sourceSetName] ?: primaryArtifactFile)) + } + + override val sourceSets: List = + sourceSetsImpl.values.toList() + + override fun getSourceSet(name: String): CompositeMetadataArtifact.SourceSet { + return findSourceSet(name) + ?: throw IllegalArgumentException("No SourceSet with name $name found. Known SourceSets: ${sourceSetsImpl.keys}") + } + + override fun findSourceSet(name: String): CompositeMetadataArtifact.SourceSet? = + sourceSetsImpl[name] + + + override fun close() { + sourceSetsImpl.values.forEach { it.close() } + } + } + + private inner class SourceSetImpl( + override val artifactHandle: CompositeMetadataArtifact.ArtifactHandle, + override val sourceSetName: String, + private val artifactFile: ArtifactFile + ) : CompositeMetadataArtifact.SourceSet, Closeable { + + override val metadataLibrary: CompositeMetadataArtifact.MetadataLibrary? by lazy { + /* + There are published multiplatform libraries that indeed suppress, disable certain compilations. + In this scenario, the sourceSetName might still be mentioned in the artifact, but there will be no + metadata-library packaged into the composite artifact. + + In this case, return null + */ + if (artifactFile.containsDirectory(sourceSetName)) MetadataLibraryImpl(artifactHandle, this, artifactFile) else null + } + + override val cinteropMetadataLibraries: List by lazy { + val cinteropMetadataDirectory = kotlinProjectStructureMetadata.sourceSetCInteropMetadataDirectory[sourceSetName] + ?: return@lazy emptyList() + + val cinteropMetadataDirectoryPath = ensureValidZipDirectoryPath(cinteropMetadataDirectory) + val cinteropEntries = artifactFile.zip.listDescendants(cinteropMetadataDirectoryPath) + + val cinteropLibraryNames = cinteropEntries.map { entry -> + entry.name.removePrefix(cinteropMetadataDirectoryPath).split("/", limit = 2).first() + }.toSet() + + cinteropLibraryNames.map { cinteropLibraryName -> + CInteropMetadataLibraryImpl(artifactHandle, this, cinteropLibraryName, artifactFile) + } + } + + override fun close() { + artifactFile.close() + } + } + + private inner class MetadataLibraryImpl( + override val artifactHandle: CompositeMetadataArtifact.ArtifactHandle, + override val sourceSet: CompositeMetadataArtifact.SourceSet, + private val artifactFile: ArtifactFile + ) : CompositeMetadataArtifact.MetadataLibrary { + + override val archiveExtension: String + get() = kotlinProjectStructureMetadata.sourceSetBinaryLayout[sourceSet.sourceSetName]?.archiveExtension + ?: SourceSetMetadataLayout.METADATA.archiveExtension + + /** + * Example: + * org.jetbrains.sample-sampleLibrary-1.0.0-SNAPSHOT-appleAndLinuxMain.klib + */ + override val relativeFile: File = File(buildString { + append(artifactHandle.moduleDependencyIdentifier) + append("-") + append(artifactHandle.moduleDependencyVersion) + append("-") + append(sourceSet.sourceSetName) + append(".") + append(archiveExtension) + }) + + override fun copyTo(file: File): Boolean { + require(file.extension == archiveExtension) { + "Expected file.extension == '$archiveExtension'. Found ${file.extension}" + } + + val libraryPath = "${sourceSet.sourceSetName}/" + if (!artifactFile.containsDirectory(libraryPath)) return false + file.parentFile.mkdirs() + artifactFile.zip.copyPartially(file, libraryPath) + + return true + } + } + + private inner class CInteropMetadataLibraryImpl( + override val artifactHandle: CompositeMetadataArtifact.ArtifactHandle, + override val sourceSet: CompositeMetadataArtifact.SourceSet, + override val cinteropLibraryName: String, + private val artifact: ArtifactFile, + ) : CompositeMetadataArtifact.CInteropMetadataLibrary { + + override val archiveExtension: String + get() = SourceSetMetadataLayout.KLIB.archiveExtension + + /** + * Example: + * org.jetbrains.sample-sampleLibrary-1.0.0-SNAPSHOT-appleAndLinuxMain-cinterop/ + * org.jetbrains.sample_sampleLibrary-cinterop-simple.klib + */ + override val relativeFile: File = File(buildString { + append(artifactHandle.moduleDependencyIdentifier) + append("-") + append(artifactHandle.moduleDependencyVersion) + append("-") + append(sourceSet.sourceSetName) + append("-cinterop") + }).resolve("$cinteropLibraryName.${archiveExtension}") + + override fun copyTo(file: File): Boolean { + require(file.extension == archiveExtension) { + "Expected 'file.extension == '${SourceSetMetadataLayout.KLIB.archiveExtension}'. Found ${file.extension}" + } + + val sourceSetName = sourceSet.sourceSetName + val cinteropMetadataDirectory = kotlinProjectStructureMetadata.sourceSetCInteropMetadataDirectory[sourceSetName] + ?: error("Missing CInteropMetadataDirectory for SourceSet $sourceSetName") + val cinteropMetadataDirectoryPath = ensureValidZipDirectoryPath(cinteropMetadataDirectory) + + val libraryPath = "$cinteropMetadataDirectoryPath$cinteropLibraryName/" + if (!artifact.containsDirectory(libraryPath)) return false + file.parentFile.mkdirs() + artifact.zip.copyPartially(file, "$cinteropMetadataDirectoryPath$cinteropLibraryName/") + + return true + } + } + + /** + * Interface to the underlying [zip][file] that only opens the file lazily and keeps references to + * all [entries] and infers all potential directory paths (see [directoryPaths] and [containsDirectory]) + */ + private class ArtifactFile(private val file: File) : Closeable { + + private var isClosed = false + + private val lazyZip = lazy { + ensureNotClosed() + ZipFile(file) + } + + val zip: ZipFile get() = lazyZip.value + + val entries: List by lazy { zip.entries().toList() } + + /** + * All potential directory paths, including inferred directory paths when the [zip] file does + * not include directory entries. + * @see collectAllDirectoryPaths + */ + val directoryPaths: Set by lazy { collectAllDirectoryPaths(entries) } + + /** + * Check if the underlying [zip] file contains this directory. + * Note: This check also works for zip files that did not include directory entries. + * This will return true, if any other zip-entry is placed inside this directory [path] + */ + fun containsDirectory(path: String): Boolean { + val validPath = ensureValidZipDirectoryPath(path) + if (zip.getEntry(validPath) != null) return true + return validPath in directoryPaths + } + + private fun ensureNotClosed() { + if (isClosed) throw IOException("LazyZipFile is already closed!") + } + + override fun close() { + isClosed = true + if (lazyZip.isInitialized()) { + lazyZip.value.close() + } + } + } +} + +/** + * Zip files are not **forced** to include entries for directories. + * In order to do preliminary checks, if some directory is present in Zip Files it is + * often useful to infer the directories included in any Zip File by looking into file entries + * and inferring their directories. + */ +private fun collectAllDirectoryPaths(entries: List): Set { + /* + The 'root' directory is represented as empty String in ZipFile + */ + val set = hashSetOf("") + + entries.forEach { entry -> + if (entry.isDirectory) { + set.add(entry.name) + return@forEach + } + + /* Collect all 'intermediate' directories found by looking at the files path */ + val pathParts = entry.name.split("/") + pathParts.runningReduce { currentPath, nextPart -> + set.add("$currentPath/") + "$currentPath/$nextPart" + } + } + return set +} + diff --git a/libraries/tools/kotlin-gradle-plugin/src/functionalTest/kotlin/org/jetbrains/kotlin/gradle/CompositeMetadataArtifactTest.kt b/libraries/tools/kotlin-gradle-plugin/src/functionalTest/kotlin/org/jetbrains/kotlin/gradle/CompositeMetadataArtifactTest.kt new file mode 100644 index 00000000000..afe1ff5abec --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin/src/functionalTest/kotlin/org/jetbrains/kotlin/gradle/CompositeMetadataArtifactTest.kt @@ -0,0 +1,320 @@ +/* + * Copyright 2010-2021 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. + */ + +@file:Suppress("FunctionName") + +package org.jetbrains.kotlin.gradle + +import org.gradle.kotlin.dsl.support.unzipTo +import org.gradle.kotlin.dsl.support.zipTo +import org.jetbrains.kotlin.gradle.plugin.mpp.* +import org.jetbrains.kotlin.gradle.plugin.mpp.SourceSetMetadataLayout.KLIB +import org.jetbrains.kotlin.gradle.plugin.mpp.SourceSetMetadataLayout.METADATA +import org.junit.Rule +import org.junit.rules.TemporaryFolder +import java.io.File +import kotlin.test.* + +class CompositeMetadataArtifactTest { + + @get:Rule + val temporaryFolder = TemporaryFolder() + + @Test + fun `empty jar - contains no metadataLibrary and no cinteropMetadataLibraries`() { + val primaryArtifactContent = temporaryFolder.newFolder() + val primaryArtifactFile = temporaryFolder.newFile("metadata.jar") + zipTo(primaryArtifactFile, primaryArtifactContent) + assertTrue(primaryArtifactFile.isFile, "Expected primaryArtifactFile.isFile") + + val metadataArtifact = CompositeMetadataArtifactImpl( + moduleDependencyIdentifier = ModuleDependencyIdentifier("test-group", "test-module"), + moduleDependencyVersion = "1.0.0", + kotlinProjectStructureMetadata = createProjectStructureMetadata( + sourceSetNames = mutableSetOf("testSourceSetName"), + sourceSetBinaryLayout = mapOf("testSourceSetName" to KLIB), + sourceSetCInteropMetadataDirectory = mapOf("testSourceSetName" to "cinterop/testSourceSetName") + ), + primaryArtifactFile = primaryArtifactFile, + hostSpecificArtifactFilesBySourceSetName = emptyMap() + ) + + + metadataArtifact.read { artifactHandle -> + if (artifactHandle.sourceSets.size != 1) fail("Expected one SourceSet in metadataArtifact") + val sourceSet = artifactHandle.sourceSets.first() + assertEquals("testSourceSetName", sourceSet.sourceSetName) + assertNull(sourceSet.metadataLibrary, "Expected no 'metadataLibrary' listed for SourceSet") + + if (sourceSet.cinteropMetadataLibraries.isNotEmpty()) { + fail( + "Expected no 'cinteropMetadataLibraries' in 'testSourceSet'. " + + "Found ${sourceSet.cinteropMetadataLibraries.map { it.cinteropLibraryName }}" + ) + } + } + } + + @Test + fun `stub metadata library - can be unzipped`() { + val testSourceSetName = "testSourceSetName" + val primaryArtifactContent = temporaryFolder.newFolder() + val stubFile = primaryArtifactContent.resolve(testSourceSetName).resolve("stub.txt") + stubFile.parentFile.mkdirs() + stubFile.writeText("stub!") + + val primaryArtifactFile = temporaryFolder.newFile("metadata.jar") + zipTo(primaryArtifactFile, primaryArtifactContent) + assertTrue(primaryArtifactFile.isFile, "Expected primaryArtifactFile.isFile") + + val metadataArtifact = CompositeMetadataArtifactImpl( + moduleDependencyIdentifier = ModuleDependencyIdentifier("test-group", "test-module"), + moduleDependencyVersion = "1.0.0", + kotlinProjectStructureMetadata = createProjectStructureMetadata( + sourceSetBinaryLayout = mapOf(testSourceSetName to KLIB), + sourceSetCInteropMetadataDirectory = emptyMap(), + ), + primaryArtifactFile = primaryArtifactFile, + hostSpecificArtifactFilesBySourceSetName = emptyMap() + ) + + metadataArtifact.read { artifactHandle -> + if (artifactHandle.sourceSets.size != 1) + fail("Expected exactly one SourceSet in ${artifactHandle.sourceSets.map { it.sourceSetName }}") + + val sourceSet = artifactHandle.getSourceSet(testSourceSetName) + val metadataOutputDirectory = temporaryFolder.newFolder() + val metadataFile = metadataOutputDirectory.resolve("testSourceSet.klib") + + val metadataLibrary = sourceSet.metadataLibrary ?: fail("Missing metadataLibrary for ${sourceSet.sourceSetName}") + assertTrue(metadataLibrary.copyTo(metadataFile), "Expected 'copyTo' to perform copy action") + assertTrue(metadataFile.isFile) + + val unzippedMetadataFile = metadataOutputDirectory.resolve("unzipped") + unzipTo(unzippedMetadataFile, metadataFile) + + assertEquals(setOf(unzippedMetadataFile.resolve("stub.txt")), unzippedMetadataFile.listFiles().orEmpty().toSet()) + assertEquals("stub!", unzippedMetadataFile.resolve("stub.txt").readText()) + } + } + + @Test + fun `empty jar - returns no cinteropMetadataLibraries`() { + val primaryArtifactContent = temporaryFolder.newFolder() + val primaryArtifactFile = temporaryFolder.newFile("metadata.jar") + zipTo(primaryArtifactFile, primaryArtifactContent) + assertTrue(primaryArtifactFile.isFile, "Expected primaryArtifactFile.isFile") + + val metadataArtifact = CompositeMetadataArtifactImpl( + moduleDependencyIdentifier = ModuleDependencyIdentifier("test-group", "test-module"), + moduleDependencyVersion = "1.0.0", + kotlinProjectStructureMetadata = createProjectStructureMetadata( + sourceSetNames = setOf("testSourceSetName") + ), + primaryArtifactFile = primaryArtifactFile, + hostSpecificArtifactFilesBySourceSetName = emptyMap() + ) + + metadataArtifact.read { artifactHandle -> + val sourceSet = artifactHandle.getSourceSet("testSourceSetName") + assertEquals(listOf(sourceSet), artifactHandle.sourceSets) + + assertEquals(emptyList(), sourceSet.cinteropMetadataLibraries, "Expected empty cinteropMetadataLibraries") + } + } + + @Test + fun `copy metadataLibrary`() { + /* Setup Artifact content */ + val primaryArtifactContent = temporaryFolder.newFolder() + + primaryArtifactContent.resolve("sourceSetA/sourceSetAStub1.txt") + .withParentDirectoriesCreated() + .writeText("Content of sourceSetA stub1") + + primaryArtifactContent.resolve("sourceSetA/nested/sourceSetAStub2.txt") + .withParentDirectoriesCreated() + .writeText("Content of sourceSetB stub2") + + primaryArtifactContent.resolve("sourceSetB/sourceSetBStub1.txt") + .withParentDirectoriesCreated() + .writeText("Content of sourceSetB stub1") + + primaryArtifactContent.resolve("sourceSetB/nested/sourceSetBStub2.txt") + .withParentDirectoriesCreated() + .writeText("Content of sourceSetB stub2") + + /* Create metadata jar */ + val primaryArtifactFile = temporaryFolder.newFile("metadata.jar") + zipTo(primaryArtifactFile, primaryArtifactContent) + + val metadataArtifact = CompositeMetadataArtifactImpl( + moduleDependencyIdentifier = ModuleDependencyIdentifier("test-group", "test-module"), + moduleDependencyVersion = "1.0.0", + kotlinProjectStructureMetadata = createProjectStructureMetadata( + sourceSetBinaryLayout = mapOf("sourceSetA" to KLIB, "sourceSetB" to METADATA) + ), + primaryArtifactFile = primaryArtifactFile, + hostSpecificArtifactFilesBySourceSetName = emptyMap() + ) + + metadataArtifact.read { artifactHandle -> + val metadataOutputDirectory = temporaryFolder.newFolder() + + /* Extract and assert sourceSetA */ + artifactHandle.getSourceSet("sourceSetA").also { sourceSetA -> + val sourceSetAMetadataFile = metadataOutputDirectory.resolve("sourceSetA.klib") + assertNotNull(sourceSetA.metadataLibrary).copyTo(sourceSetAMetadataFile) + assertTrue(sourceSetAMetadataFile.isFile, "Expected sourceSetAMetadataFile.isFile") + assertEquals( + KLIB.archiveExtension, sourceSetAMetadataFile.extension, + "Expected correct archiveExtension for extracted sourceSetA" + ) + assertZipContentEquals( + temporaryFolder, + primaryArtifactContent.resolve("sourceSetA"), sourceSetAMetadataFile, + "Expected correct content of extracted 'sourceSetA'" + ) + } + + /* Extract and assert sourceSetB */ + artifactHandle.getSourceSet("sourceSetB").also { sourceSetB -> + val sourceSetBMetadataFile = metadataOutputDirectory.resolve("sourceSetB.jar") + assertNotNull(sourceSetB.metadataLibrary).copyTo(sourceSetBMetadataFile) + assertTrue(sourceSetBMetadataFile.isFile, "Expected sourceSetBMetadataFile.isFile") + assertEquals( + METADATA.archiveExtension, sourceSetBMetadataFile.extension, + "Expected correct archiveExtension for extracted sourceSetB" + ) + assertZipContentEquals( + temporaryFolder, + primaryArtifactContent.resolve("sourceSetB"), sourceSetBMetadataFile, + "Expected correct content of extracted 'sourceSetA'" + ) + } + } + } + + @Test + fun `copy cinteropMetadataLibraries`() { + /* Setup Artifact content */ + val primaryArtifactContent = temporaryFolder.newFolder() + + primaryArtifactContent.resolve("sourceSetA-cinterop/interopA0/stub0") + .withParentDirectoriesCreated() + .writeText("stub0 content") + + primaryArtifactContent.resolve("sourceSetA-cinterop/interopA0/nested/stub1") + .withParentDirectoriesCreated() + .writeText("stub1 content") + + primaryArtifactContent.resolve("sourceSetA-cinterop/interopA1/stub2") + .withParentDirectoriesCreated() + .writeText("stub2 content") + + primaryArtifactContent.resolve("nested/sourceSetB/interops/interopB0/stub3") + .withParentDirectoriesCreated() + .writeText("stub3 content") + + /* Create metadata jar */ + val primaryArtifactFile = temporaryFolder.newFile("metadata.jar") + zipTo(primaryArtifactFile, primaryArtifactContent) + + val metadataArtifact = CompositeMetadataArtifactImpl( + moduleDependencyIdentifier = ModuleDependencyIdentifier("test-group", "test-module"), + moduleDependencyVersion = "1.0.0", + kotlinProjectStructureMetadata = createProjectStructureMetadata( + sourceSetCInteropMetadataDirectory = mapOf( + "sourceSetA" to "sourceSetA-cinterop", + "sourceSetB" to "nested/sourceSetB/interops/" + ) + ), + primaryArtifactFile = primaryArtifactFile, + hostSpecificArtifactFilesBySourceSetName = emptyMap() + ) + + metadataArtifact.read { artifactHandle -> + /* Assertions on sourceSetA */ + artifactHandle.getSourceSet("sourceSetA").also { sourceSetA -> + val sourceSetAOutputDirectory = temporaryFolder.newFolder() + val sourceSetAInteropMetadataFiles = sourceSetA.cinteropMetadataLibraries.map { cinteropLibrary -> + sourceSetAOutputDirectory.resolve("${cinteropLibrary.cinteropLibraryName}.klib").also { file -> + cinteropLibrary.copyTo(file) + } + } + assertEquals(2, sourceSetAInteropMetadataFiles.size, "Expected 2 cinterops in sourceSetA") + + /* Assertions on interopA0 */ + run { + val interopA0MetadataFile = sourceSetAInteropMetadataFiles.firstOrNull { it.name == "interopA0.klib" } + ?: fail("Failed to find 'interopA0.klib'") + assertZipContentEquals( + temporaryFolder, + primaryArtifactContent.resolve("sourceSetA-cinterop/interopA0"), interopA0MetadataFile, + "Expected correct content for extracted 'interopA0'" + ) + } + + /* Assertions on interopA1 */ + run { + val interopA1MetadataFile = sourceSetAInteropMetadataFiles.firstOrNull { it.name == "interopA1.klib" } + ?: fail("Failed to find 'interopA1.klib") + assertZipContentEquals( + temporaryFolder, + primaryArtifactContent.resolve("sourceSetA-cinterop/interopA1"), interopA1MetadataFile, + "Expected correct content for extracted 'interopA1'" + ) + } + } + + /* Assertions on sourceSetB */ + artifactHandle.getSourceSet("sourceSetB").also { sourceSetB -> + val sourceSetBOutputDirectory = temporaryFolder.newFolder() + val sourceSetBInteropMetadataFiles = sourceSetB.cinteropMetadataLibraries.map { cinteropLibrary -> + sourceSetBOutputDirectory.resolve("${cinteropLibrary.cinteropLibraryName}.klib").also { file -> + cinteropLibrary.copyTo(file) + } + } + assertEquals(1, sourceSetBInteropMetadataFiles.size, "Expected only one cinterop in sourceSetB") + val interopB0MetadataFile = sourceSetBInteropMetadataFiles.firstOrNull { it.name == "interopB0.klib" } + ?: fail("Failed to find 'interopB0.klib'") + assertZipContentEquals( + temporaryFolder, + primaryArtifactContent.resolve("nested/sourceSetB/interops/interopB0"), interopB0MetadataFile, + "Expected correct content for extracted 'interopB0'" + ) + } + } + } +} + +private fun createProjectStructureMetadata( + sourceSetNamesByVariantName: Map> = emptyMap(), + sourceSetsDependsOnRelation: Map> = emptyMap(), + sourceSetCInteropMetadataDirectory: Map = emptyMap(), + sourceSetBinaryLayout: Map = emptyMap(), + sourceSetModuleDependencies: Map> = emptyMap(), + hostSpecificSourceSets: Set = emptySet(), + sourceSetNames: Set = hostSpecificSourceSets + + sourceSetNamesByVariantName.values.flatten() + + sourceSetsDependsOnRelation.keys + + sourceSetCInteropMetadataDirectory.keys + + sourceSetBinaryLayout.keys + + sourceSetModuleDependencies.keys, + isPublishedAsRoot: Boolean = true, +): KotlinProjectStructureMetadata { + return KotlinProjectStructureMetadata( + sourceSetNamesByVariantName = sourceSetNamesByVariantName, + sourceSetsDependsOnRelation = sourceSetsDependsOnRelation, + sourceSetBinaryLayout = sourceSetBinaryLayout, + sourceSetCInteropMetadataDirectory = sourceSetCInteropMetadataDirectory, + sourceSetModuleDependencies = sourceSetModuleDependencies, + hostSpecificSourceSets = hostSpecificSourceSets, + sourceSetNames = sourceSetNames, + isPublishedAsRoot = isPublishedAsRoot + ) +} + +private fun File.withParentDirectoriesCreated(): File = apply { parentFile.mkdirs() }