diff --git a/README.md b/README.md index ffb58a154..0ae62cac5 100644 --- a/README.md +++ b/README.md @@ -98,9 +98,8 @@ Tapping a library opens its details — inline, in a dialog, or in a bottom shee ## Latest releases 🛠 -- Compose 1.11.x | New UI | AGP 9 | Kotlin 2.4 | Compile 37 | [v15.0.4](https://github.com/mikepenz/AboutLibraries/tree/15.0.4) +- Compose 1.11.x | New UI | AGP 9 | Kotlin 2.4 | Compile 37 | [v15.0.4](https://github.com/mikepenz/AboutLibraries/tree/15.0.4) / [v15.1.0-b01](https://github.com/mikepenz/AboutLibraries/tree/15.1.0-b01) - Compose 1.10.x | AGP 9 | [v14.1.0](https://github.com/mikepenz/AboutLibraries/tree/14.1.0) -- Compose 1.10.x | [v13.2.1](https://github.com/mikepenz/AboutLibraries/tree/13.2.1) ## Gradle Plugin @@ -218,6 +217,16 @@ aboutLibraries { // Enable inclusion of `platform` dependencies in the library report includePlatform = true + + // Enable reporting of the Kotlin targets each library is consumed by, as a `targets` array + // on every library (e.g. ["android", "jvm", "iosX64"]). Target names are taken from the + // Kotlin target model, so a consumer can narrow the rendered list to what the running + // target links against: `libs.libraries.filter { "iosArm64" in it.targets }`. + // Only multiplatform projects report anything — an Android-only or JVM-only project builds + // a single implicit target, so every library reports an empty array. + // Disabled by default; when disabled the `targets` field is omitted from the output entirely. + // It can also be dropped per export via `excludeFields.add("Library.targets")`. + includeTargets = false } export { @@ -269,6 +278,10 @@ aboutLibraries { duplicationMode = com.mikepenz.aboutlibraries.plugin.DuplicateMode.MERGE // Configure the duplication rule, to match "duplicates" with duplicationRule = com.mikepenz.aboutlibraries.plugin.DuplicateRule.SIMPLE + // Report the per-platform artifacts of a Kotlin Multiplatform publication under the declared + // root coordinate (`androidx.collection:collection`, not `androidx.collection:collection-jvm`). + // Unrelated to Android build variants — see `filterVariants` for those. + mergePlatformArtifacts = true } } ``` diff --git a/aboutlibraries-core/api/aboutlibraries-core.klib.api b/aboutlibraries-core/api/aboutlibraries-core.klib.api index 69cbbfdef..22cd4ecfc 100644 --- a/aboutlibraries-core/api/aboutlibraries-core.klib.api +++ b/aboutlibraries-core/api/aboutlibraries-core.klib.api @@ -65,7 +65,7 @@ final class com.mikepenz.aboutlibraries.entity/Funding { // com.mikepenz.aboutli } final class com.mikepenz.aboutlibraries.entity/Library { // com.mikepenz.aboutlibraries.entity/Library|null[0] - constructor (kotlin/String, kotlin/String?, kotlin/String, kotlin/String?, kotlin/String?, kotlin.collections/List, com.mikepenz.aboutlibraries.entity/Organization?, com.mikepenz.aboutlibraries.entity/Scm?, kotlin.collections/Set = ..., kotlin.collections/Set = ..., kotlin/String? = ...) // com.mikepenz.aboutlibraries.entity/Library.|(kotlin.String;kotlin.String?;kotlin.String;kotlin.String?;kotlin.String?;kotlin.collections.List;com.mikepenz.aboutlibraries.entity.Organization?;com.mikepenz.aboutlibraries.entity.Scm?;kotlin.collections.Set;kotlin.collections.Set;kotlin.String?){}[0] + constructor (kotlin/String, kotlin/String?, kotlin/String, kotlin/String?, kotlin/String?, kotlin.collections/List, com.mikepenz.aboutlibraries.entity/Organization?, com.mikepenz.aboutlibraries.entity/Scm?, kotlin.collections/Set = ..., kotlin.collections/Set = ..., kotlin/String? = ..., kotlin.collections/Set = ...) // com.mikepenz.aboutlibraries.entity/Library.|(kotlin.String;kotlin.String?;kotlin.String;kotlin.String?;kotlin.String?;kotlin.collections.List;com.mikepenz.aboutlibraries.entity.Organization?;com.mikepenz.aboutlibraries.entity.Scm?;kotlin.collections.Set;kotlin.collections.Set;kotlin.String?;kotlin.collections.Set){}[0] final val artifactId // com.mikepenz.aboutlibraries.entity/Library.artifactId|{}artifactId[0] final fun (): kotlin/String // com.mikepenz.aboutlibraries.entity/Library.artifactId.|(){}[0] @@ -89,6 +89,8 @@ final class com.mikepenz.aboutlibraries.entity/Library { // com.mikepenz.aboutli final fun (): com.mikepenz.aboutlibraries.entity/Scm? // com.mikepenz.aboutlibraries.entity/Library.scm.|(){}[0] final val tag // com.mikepenz.aboutlibraries.entity/Library.tag|{}tag[0] final fun (): kotlin/String? // com.mikepenz.aboutlibraries.entity/Library.tag.|(){}[0] + final val targets // com.mikepenz.aboutlibraries.entity/Library.targets|{}targets[0] + final fun (): kotlin.collections/Set // com.mikepenz.aboutlibraries.entity/Library.targets.|(){}[0] final val uniqueId // com.mikepenz.aboutlibraries.entity/Library.uniqueId|{}uniqueId[0] final fun (): kotlin/String // com.mikepenz.aboutlibraries.entity/Library.uniqueId.|(){}[0] final val website // com.mikepenz.aboutlibraries.entity/Library.website|{}website[0] @@ -97,6 +99,7 @@ final class com.mikepenz.aboutlibraries.entity/Library { // com.mikepenz.aboutli final fun component1(): kotlin/String // com.mikepenz.aboutlibraries.entity/Library.component1|component1(){}[0] final fun component10(): kotlin.collections/Set // com.mikepenz.aboutlibraries.entity/Library.component10|component10(){}[0] final fun component11(): kotlin/String? // com.mikepenz.aboutlibraries.entity/Library.component11|component11(){}[0] + final fun component12(): kotlin.collections/Set // com.mikepenz.aboutlibraries.entity/Library.component12|component12(){}[0] final fun component2(): kotlin/String? // com.mikepenz.aboutlibraries.entity/Library.component2|component2(){}[0] final fun component3(): kotlin/String // com.mikepenz.aboutlibraries.entity/Library.component3|component3(){}[0] final fun component4(): kotlin/String? // com.mikepenz.aboutlibraries.entity/Library.component4|component4(){}[0] @@ -105,7 +108,7 @@ final class com.mikepenz.aboutlibraries.entity/Library { // com.mikepenz.aboutli final fun component7(): com.mikepenz.aboutlibraries.entity/Organization? // com.mikepenz.aboutlibraries.entity/Library.component7|component7(){}[0] final fun component8(): com.mikepenz.aboutlibraries.entity/Scm? // com.mikepenz.aboutlibraries.entity/Library.component8|component8(){}[0] final fun component9(): kotlin.collections/Set // com.mikepenz.aboutlibraries.entity/Library.component9|component9(){}[0] - final fun copy(kotlin/String = ..., kotlin/String? = ..., kotlin/String = ..., kotlin/String? = ..., kotlin/String? = ..., kotlin.collections/List = ..., com.mikepenz.aboutlibraries.entity/Organization? = ..., com.mikepenz.aboutlibraries.entity/Scm? = ..., kotlin.collections/Set = ..., kotlin.collections/Set = ..., kotlin/String? = ...): com.mikepenz.aboutlibraries.entity/Library // com.mikepenz.aboutlibraries.entity/Library.copy|copy(kotlin.String;kotlin.String?;kotlin.String;kotlin.String?;kotlin.String?;kotlin.collections.List;com.mikepenz.aboutlibraries.entity.Organization?;com.mikepenz.aboutlibraries.entity.Scm?;kotlin.collections.Set;kotlin.collections.Set;kotlin.String?){}[0] + final fun copy(kotlin/String = ..., kotlin/String? = ..., kotlin/String = ..., kotlin/String? = ..., kotlin/String? = ..., kotlin.collections/List = ..., com.mikepenz.aboutlibraries.entity/Organization? = ..., com.mikepenz.aboutlibraries.entity/Scm? = ..., kotlin.collections/Set = ..., kotlin.collections/Set = ..., kotlin/String? = ..., kotlin.collections/Set = ...): com.mikepenz.aboutlibraries.entity/Library // com.mikepenz.aboutlibraries.entity/Library.copy|copy(kotlin.String;kotlin.String?;kotlin.String;kotlin.String?;kotlin.String?;kotlin.collections.List;com.mikepenz.aboutlibraries.entity.Organization?;com.mikepenz.aboutlibraries.entity.Scm?;kotlin.collections.Set;kotlin.collections.Set;kotlin.String?;kotlin.collections.Set){}[0] final fun equals(kotlin/Any?): kotlin/Boolean // com.mikepenz.aboutlibraries.entity/Library.equals|equals(kotlin.Any?){}[0] final fun hashCode(): kotlin/Int // com.mikepenz.aboutlibraries.entity/Library.hashCode|hashCode(){}[0] final fun toString(): kotlin/String // com.mikepenz.aboutlibraries.entity/Library.toString|toString(){}[0] diff --git a/aboutlibraries-core/api/android/aboutlibraries-core.api b/aboutlibraries-core/api/android/aboutlibraries-core.api index c7b961153..8f96acb48 100644 --- a/aboutlibraries-core/api/android/aboutlibraries-core.api +++ b/aboutlibraries-core/api/android/aboutlibraries-core.api @@ -93,11 +93,12 @@ public final class com/mikepenz/aboutlibraries/entity/Funding$Companion { public final class com/mikepenz/aboutlibraries/entity/Library { public static final field Companion Lcom/mikepenz/aboutlibraries/entity/Library$Companion; - public fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/List;Lcom/mikepenz/aboutlibraries/entity/Organization;Lcom/mikepenz/aboutlibraries/entity/Scm;Ljava/util/Set;Ljava/util/Set;Ljava/lang/String;)V - public synthetic fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/List;Lcom/mikepenz/aboutlibraries/entity/Organization;Lcom/mikepenz/aboutlibraries/entity/Scm;Ljava/util/Set;Ljava/util/Set;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/List;Lcom/mikepenz/aboutlibraries/entity/Organization;Lcom/mikepenz/aboutlibraries/entity/Scm;Ljava/util/Set;Ljava/util/Set;Ljava/lang/String;Ljava/util/Set;)V + public synthetic fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/List;Lcom/mikepenz/aboutlibraries/entity/Organization;Lcom/mikepenz/aboutlibraries/entity/Scm;Ljava/util/Set;Ljava/util/Set;Ljava/lang/String;Ljava/util/Set;ILkotlin/jvm/internal/DefaultConstructorMarker;)V public final fun component1 ()Ljava/lang/String; public final fun component10 ()Ljava/util/Set; public final fun component11 ()Ljava/lang/String; + public final fun component12 ()Ljava/util/Set; public final fun component2 ()Ljava/lang/String; public final fun component3 ()Ljava/lang/String; public final fun component4 ()Ljava/lang/String; @@ -106,8 +107,8 @@ public final class com/mikepenz/aboutlibraries/entity/Library { public final fun component7 ()Lcom/mikepenz/aboutlibraries/entity/Organization; public final fun component8 ()Lcom/mikepenz/aboutlibraries/entity/Scm; public final fun component9 ()Ljava/util/Set; - public final fun copy (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/List;Lcom/mikepenz/aboutlibraries/entity/Organization;Lcom/mikepenz/aboutlibraries/entity/Scm;Ljava/util/Set;Ljava/util/Set;Ljava/lang/String;)Lcom/mikepenz/aboutlibraries/entity/Library; - public static synthetic fun copy$default (Lcom/mikepenz/aboutlibraries/entity/Library;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/List;Lcom/mikepenz/aboutlibraries/entity/Organization;Lcom/mikepenz/aboutlibraries/entity/Scm;Ljava/util/Set;Ljava/util/Set;Ljava/lang/String;ILjava/lang/Object;)Lcom/mikepenz/aboutlibraries/entity/Library; + public final fun copy (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/List;Lcom/mikepenz/aboutlibraries/entity/Organization;Lcom/mikepenz/aboutlibraries/entity/Scm;Ljava/util/Set;Ljava/util/Set;Ljava/lang/String;Ljava/util/Set;)Lcom/mikepenz/aboutlibraries/entity/Library; + public static synthetic fun copy$default (Lcom/mikepenz/aboutlibraries/entity/Library;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/List;Lcom/mikepenz/aboutlibraries/entity/Organization;Lcom/mikepenz/aboutlibraries/entity/Scm;Ljava/util/Set;Ljava/util/Set;Ljava/lang/String;Ljava/util/Set;ILjava/lang/Object;)Lcom/mikepenz/aboutlibraries/entity/Library; public fun equals (Ljava/lang/Object;)Z public final fun getArtifactId ()Ljava/lang/String; public final fun getArtifactVersion ()Ljava/lang/String; @@ -120,6 +121,7 @@ public final class com/mikepenz/aboutlibraries/entity/Library { public final fun getOrganization ()Lcom/mikepenz/aboutlibraries/entity/Organization; public final fun getScm ()Lcom/mikepenz/aboutlibraries/entity/Scm; public final fun getTag ()Ljava/lang/String; + public final fun getTargets ()Ljava/util/Set; public final fun getUniqueId ()Ljava/lang/String; public final fun getWebsite ()Ljava/lang/String; public fun hashCode ()I diff --git a/aboutlibraries-core/api/jvm/aboutlibraries-core.api b/aboutlibraries-core/api/jvm/aboutlibraries-core.api index b95d8286a..aeb111c94 100644 --- a/aboutlibraries-core/api/jvm/aboutlibraries-core.api +++ b/aboutlibraries-core/api/jvm/aboutlibraries-core.api @@ -93,11 +93,12 @@ public final class com/mikepenz/aboutlibraries/entity/Funding$Companion { public final class com/mikepenz/aboutlibraries/entity/Library { public static final field Companion Lcom/mikepenz/aboutlibraries/entity/Library$Companion; - public fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/List;Lcom/mikepenz/aboutlibraries/entity/Organization;Lcom/mikepenz/aboutlibraries/entity/Scm;Ljava/util/Set;Ljava/util/Set;Ljava/lang/String;)V - public synthetic fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/List;Lcom/mikepenz/aboutlibraries/entity/Organization;Lcom/mikepenz/aboutlibraries/entity/Scm;Ljava/util/Set;Ljava/util/Set;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/List;Lcom/mikepenz/aboutlibraries/entity/Organization;Lcom/mikepenz/aboutlibraries/entity/Scm;Ljava/util/Set;Ljava/util/Set;Ljava/lang/String;Ljava/util/Set;)V + public synthetic fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/List;Lcom/mikepenz/aboutlibraries/entity/Organization;Lcom/mikepenz/aboutlibraries/entity/Scm;Ljava/util/Set;Ljava/util/Set;Ljava/lang/String;Ljava/util/Set;ILkotlin/jvm/internal/DefaultConstructorMarker;)V public final fun component1 ()Ljava/lang/String; public final fun component10 ()Ljava/util/Set; public final fun component11 ()Ljava/lang/String; + public final fun component12 ()Ljava/util/Set; public final fun component2 ()Ljava/lang/String; public final fun component3 ()Ljava/lang/String; public final fun component4 ()Ljava/lang/String; @@ -106,8 +107,8 @@ public final class com/mikepenz/aboutlibraries/entity/Library { public final fun component7 ()Lcom/mikepenz/aboutlibraries/entity/Organization; public final fun component8 ()Lcom/mikepenz/aboutlibraries/entity/Scm; public final fun component9 ()Ljava/util/Set; - public final fun copy (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/List;Lcom/mikepenz/aboutlibraries/entity/Organization;Lcom/mikepenz/aboutlibraries/entity/Scm;Ljava/util/Set;Ljava/util/Set;Ljava/lang/String;)Lcom/mikepenz/aboutlibraries/entity/Library; - public static synthetic fun copy$default (Lcom/mikepenz/aboutlibraries/entity/Library;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/List;Lcom/mikepenz/aboutlibraries/entity/Organization;Lcom/mikepenz/aboutlibraries/entity/Scm;Ljava/util/Set;Ljava/util/Set;Ljava/lang/String;ILjava/lang/Object;)Lcom/mikepenz/aboutlibraries/entity/Library; + public final fun copy (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/List;Lcom/mikepenz/aboutlibraries/entity/Organization;Lcom/mikepenz/aboutlibraries/entity/Scm;Ljava/util/Set;Ljava/util/Set;Ljava/lang/String;Ljava/util/Set;)Lcom/mikepenz/aboutlibraries/entity/Library; + public static synthetic fun copy$default (Lcom/mikepenz/aboutlibraries/entity/Library;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/List;Lcom/mikepenz/aboutlibraries/entity/Organization;Lcom/mikepenz/aboutlibraries/entity/Scm;Ljava/util/Set;Ljava/util/Set;Ljava/lang/String;Ljava/util/Set;ILjava/lang/Object;)Lcom/mikepenz/aboutlibraries/entity/Library; public fun equals (Ljava/lang/Object;)Z public final fun getArtifactId ()Ljava/lang/String; public final fun getArtifactVersion ()Ljava/lang/String; @@ -120,6 +121,7 @@ public final class com/mikepenz/aboutlibraries/entity/Library { public final fun getOrganization ()Lcom/mikepenz/aboutlibraries/entity/Organization; public final fun getScm ()Lcom/mikepenz/aboutlibraries/entity/Scm; public final fun getTag ()Ljava/lang/String; + public final fun getTargets ()Ljava/util/Set; public final fun getUniqueId ()Ljava/lang/String; public final fun getWebsite ()Ljava/lang/String; public fun hashCode ()I diff --git a/aboutlibraries-core/src/androidMain/kotlin/com/mikepenz/aboutlibraries/util/AndroidParser.kt b/aboutlibraries-core/src/androidMain/kotlin/com/mikepenz/aboutlibraries/util/AndroidParser.kt index feff1f15e..b36ba5cb0 100644 --- a/aboutlibraries-core/src/androidMain/kotlin/com/mikepenz/aboutlibraries/util/AndroidParser.kt +++ b/aboutlibraries-core/src/androidMain/kotlin/com/mikepenz/aboutlibraries/util/AndroidParser.kt @@ -44,6 +44,7 @@ actual fun parseData(json: String): Result { val funding = optJSONArray("funding").forEachObject { Funding(getString("platform"), getString("url")) }.toSet() + val targets = optJSONArray("targets").forEachString { this }.toSet() val id = getString("uniqueId") Library( id, @@ -56,7 +57,8 @@ actual fun parseData(json: String): Result { scm, libLicenses, funding, - optStringOrNull("tag") + optStringOrNull("tag"), + targets ) } return Result(libraries, licenses) diff --git a/aboutlibraries-core/src/commonMain/kotlin/com/mikepenz/aboutlibraries/entity/Library.kt b/aboutlibraries-core/src/commonMain/kotlin/com/mikepenz/aboutlibraries/entity/Library.kt index 2d1e5cc32..e7ab8da79 100644 --- a/aboutlibraries-core/src/commonMain/kotlin/com/mikepenz/aboutlibraries/entity/Library.kt +++ b/aboutlibraries-core/src/commonMain/kotlin/com/mikepenz/aboutlibraries/entity/Library.kt @@ -16,6 +16,10 @@ import kotlinx.serialization.Serializable * @param scm information, linking to the repository hosting the source * @param licenses all identified licenses for this artifact * @param funding all identified funding opportunities for this artifact + * @param targets Kotlin target names this artifact is consumed by (e.g. `android`, `jvm`, `iosX64`). + * Empty unless the `collect.includeTargets` option was enabled when generating the metadata file. + * Lets a consumer narrow the list to what the running target links against, e.g. + * `libs.libraries.filter { "iosArm64" in it.targets }`. */ @Serializable data class Library( @@ -30,6 +34,7 @@ data class Library( @SerialName("licenses") val licenses: Set = emptySet(), @SerialName("funding") val funding: Set = emptySet(), @SerialName("tag") val tag: String? = null, + @SerialName("targets") val targets: Set = emptySet(), ) { /** * defines the [uniqueId]:[artifactVersion] combined diff --git a/aboutlibraries-core/src/jvmTest/kotlin/com/mikepenz/aboutlibraries/TargetsParsingTest.kt b/aboutlibraries-core/src/jvmTest/kotlin/com/mikepenz/aboutlibraries/TargetsParsingTest.kt new file mode 100644 index 000000000..7b24438d9 --- /dev/null +++ b/aboutlibraries-core/src/jvmTest/kotlin/com/mikepenz/aboutlibraries/TargetsParsingTest.kt @@ -0,0 +1,60 @@ +package com.mikepenz.aboutlibraries + +import org.junit.Test +import kotlin.test.assertEquals + +/** + * The `targets` field is only written by the Gradle plugin when `collect.includeTargets` is + * enabled, so the parser must handle both its presence and its absence within the same document. + */ +class TargetsParsingTest { + + @Test + fun testTargetsParsing() { + val result = Libs.Builder().withJson(json).build() + + assertEquals(2, result.libraries.size) + + val withTargets = result.libraries.first { it.uniqueId == "com.example:with-targets" } + assertEquals(setOf("android", "iosArm64", "jvm"), withTargets.targets) + + val withoutTargets = result.libraries.first { it.uniqueId == "com.example:without-targets" } + assertEquals(emptySet(), withoutTargets.targets) + } + + /** The point of the field: narrowing the rendered list to what the running target links against. */ + @Test + fun testFilteringByTarget() { + val result = Libs.Builder().withJson(json).build() + + assertEquals( + listOf("com.example:with-targets"), + result.libraries.filter { "iosArm64" in it.targets }.map { it.uniqueId } + ) + } + + private val json = """ + { + "libraries": [ + { + "uniqueId": "com.example:with-targets", + "artifactVersion": "1.0.0", + "name": "With Targets", + "developers": [], + "licenses": [], + "funding": [], + "targets": ["android", "iosArm64", "jvm"] + }, + { + "uniqueId": "com.example:without-targets", + "artifactVersion": "1.0.0", + "name": "Without Targets", + "developers": [], + "licenses": [], + "funding": [] + } + ], + "licenses": {} + } + """.trimIndent() +} diff --git a/aboutlibraries-core/src/multiplatformMain/kotlin/com/mikepenz/aboutlibraries/util/MultiplatformParser.kt b/aboutlibraries-core/src/multiplatformMain/kotlin/com/mikepenz/aboutlibraries/util/MultiplatformParser.kt index 7ce639ad6..48103d1c1 100644 --- a/aboutlibraries-core/src/multiplatformMain/kotlin/com/mikepenz/aboutlibraries/util/MultiplatformParser.kt +++ b/aboutlibraries-core/src/multiplatformMain/kotlin/com/mikepenz/aboutlibraries/util/MultiplatformParser.kt @@ -44,6 +44,7 @@ actual fun parseData(json: String): Result { val funding = optJSONArray("funding").forEachObject { Funding(getString("platform"), getString("url")) }.toSet() + val targets = optJSONArray("targets").forEachString { this }.toSet() val id = getString("uniqueId") Library( @@ -57,7 +58,8 @@ actual fun parseData(json: String): Result { scm, libLicenses, funding, - optString("tag") + optString("tag"), + targets ) } return Result(libraries, licenses) diff --git a/app-test/build.gradle.kts b/app-test/build.gradle.kts index 08e99e385..9c579e989 100644 --- a/app-test/build.gradle.kts +++ b/app-test/build.gradle.kts @@ -26,6 +26,7 @@ kotlin { aboutLibraries { collect { fetchRemoteLicense = true + includeTargets = true } export { prettyPrint = true diff --git a/app-test/files/aboutlibraries.json b/app-test/files/aboutlibraries.json index 419000a1b..55060f899 100644 --- a/app-test/files/aboutlibraries.json +++ b/app-test/files/aboutlibraries.json @@ -26,11 +26,14 @@ ], "funding": [ + ], + "targets": [ + "jvm" ] }, { "uniqueId": "org.jetbrains.kotlin:kotlin-stdlib", - "artifactVersion": "2.3.20", + "artifactVersion": "2.4.10", "name": "Kotlin Stdlib", "description": "Kotlin Standard Library", "website": "https://kotlinlang.org/", @@ -50,11 +53,16 @@ ], "funding": [ + ], + "targets": [ + "jvm", + "metadata", + "wasmJs" ] }, { "uniqueId": "org.jetbrains.kotlin:kotlin-stdlib-wasm-js", - "artifactVersion": "2.3.20", + "artifactVersion": "2.4.10", "name": "Kotlin Stdlib Wasm Js", "description": "Kotlin Standard Library for experimental WebAssembly JS platform", "website": "https://kotlinlang.org/", @@ -74,6 +82,9 @@ ], "funding": [ + ], + "targets": [ + "wasmJs" ] }, { @@ -93,21 +104,18 @@ "url": "https://github.com/JetBrains/intellij-community" }, "licenses": [ - "196b44647f01b6b79fdfedf9cd2caed7" + "Apache-2.0", + "fea9e903303ed8cbc7854c24956a8913" ], "funding": [ + ], + "targets": [ + "jvm" ] } ], "licenses": { - "196b44647f01b6b79fdfedf9cd2caed7": { - "name": "Apache License 2.0", - "url": "https://raw.githubusercontent.com/JetBrains/intellij-community/master/LICENSE.txt", - "content": " Apache License\n Version 2.0, January 2004\n http://www.apache.org/licenses/\n\nTERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n1. Definitions.\n\n \"License\" shall mean the terms and conditions for use, reproduction,\n and distribution as defined by Sections 1 through 9 of this document.\n\n \"Licensor\" shall mean the copyright owner or entity authorized by\n the copyright owner that is granting the License.\n\n \"Legal Entity\" shall mean the union of the acting entity and all\n other entities that control, are controlled by, or are under common\n control with that entity. For the purposes of this definition,\n \"control\" means (i) the power, direct or indirect, to cause the\n direction or management of such entity, whether by contract or\n otherwise, or (ii) ownership of fifty percent (50%) or more of the\n outstanding shares, or (iii) beneficial ownership of such entity.\n\n \"You\" (or \"Your\") shall mean an individual or Legal Entity\n exercising permissions granted by this License.\n\n \"Source\" form shall mean the preferred form for making modifications,\n including but not limited to software source code, documentation\n source, and configuration files.\n\n \"Object\" form shall mean any form resulting from mechanical\n transformation or translation of a Source form, including but\n not limited to compiled object code, generated documentation,\n and conversions to other media types.\n\n \"Work\" shall mean the work of authorship, whether in Source or\n Object form, made available under the License, as indicated by a\n copyright notice that is included in or attached to the work\n (an example is provided in the Appendix below).\n\n \"Derivative Works\" shall mean any work, whether in Source or Object\n form, that is based on (or derived from) the Work and for which the\n editorial revisions, annotations, elaborations, or other modifications\n represent, as a whole, an original work of authorship. For the purposes\n of this License, Derivative Works shall not include works that remain\n separable from, or merely link (or bind by name) to the interfaces of,\n the Work and Derivative Works thereof.\n\n \"Contribution\" shall mean any work of authorship, including\n the original version of the Work and any modifications or additions\n to that Work or Derivative Works thereof, that is intentionally\n submitted to Licensor for inclusion in the Work by the copyright owner\n or by an individual or Legal Entity authorized to submit on behalf of\n the copyright owner. For the purposes of this definition, \"submitted\"\n means any form of electronic, verbal, or written communication sent\n to the Licensor or its representatives, including but not limited to\n communication on electronic mailing lists, source code control systems,\n and issue tracking systems that are managed by, or on behalf of, the\n Licensor for the purpose of discussing and improving the Work, but\n excluding communication that is conspicuously marked or otherwise\n designated in writing by the copyright owner as \"Not a Contribution.\"\n\n \"Contributor\" shall mean Licensor and any individual or Legal Entity\n on behalf of whom a Contribution has been received by Licensor and\n subsequently incorporated within the Work.\n\n2. Grant of Copyright License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n copyright license to reproduce, prepare Derivative Works of,\n publicly display, publicly perform, sublicense, and distribute the\n Work and such Derivative Works in Source or Object form.\n\n3. Grant of Patent License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n (except as stated in this section) patent license to make, have made,\n use, offer to sell, sell, import, and otherwise transfer the Work,\n where such license applies only to those patent claims licensable\n by such Contributor that are necessarily infringed by their\n Contribution(s) alone or by combination of their Contribution(s)\n with the Work to which such Contribution(s) was submitted. If You\n institute patent litigation against any entity (including a\n cross-claim or counterclaim in a lawsuit) alleging that the Work\n or a Contribution incorporated within the Work constitutes direct\n or contributory patent infringement, then any patent licenses\n granted to You under this License for that Work shall terminate\n as of the date such litigation is filed.\n\n4. Redistribution. You may reproduce and distribute copies of the\n Work or Derivative Works thereof in any medium, with or without\n modifications, and in Source or Object form, provided that You\n meet the following conditions:\n\n (a) You must give any other recipients of the Work or\n Derivative Works a copy of this License; and\n\n (b) You must cause any modified files to carry prominent notices\n stating that You changed the files; and\n\n (c) You must retain, in the Source form of any Derivative Works\n that You distribute, all copyright, patent, trademark, and\n attribution notices from the Source form of the Work,\n excluding those notices that do not pertain to any part of\n the Derivative Works; and\n\n (d) If the Work includes a \"NOTICE\" text file as part of its\n distribution, then any Derivative Works that You distribute must\n include a readable copy of the attribution notices contained\n within such NOTICE file, excluding those notices that do not\n pertain to any part of the Derivative Works, in at least one\n of the following places: within a NOTICE text file distributed\n as part of the Derivative Works; within the Source form or\n documentation, if provided along with the Derivative Works; or,\n within a display generated by the Derivative Works, if and\n wherever such third-party notices normally appear. The contents\n of the NOTICE file are for informational purposes only and\n do not modify the License. You may add Your own attribution\n notices within Derivative Works that You distribute, alongside\n or as an addendum to the NOTICE text from the Work, provided\n that such additional attribution notices cannot be construed\n as modifying the License.\n\n You may add Your own copyright statement to Your modifications and\n may provide additional or different license terms and conditions\n for use, reproduction, or distribution of Your modifications, or\n for any such Derivative Works as a whole, provided Your use,\n reproduction, and distribution of the Work otherwise complies with\n the conditions stated in this License.\n\n5. Submission of Contributions. Unless You explicitly state otherwise,\n any Contribution intentionally submitted for inclusion in the Work\n by You to the Licensor shall be under the terms and conditions of\n this License, without any additional terms or conditions.\n Notwithstanding the above, nothing herein shall supersede or modify\n the terms of any separate license agreement you may have executed\n with Licensor regarding such Contributions.\n\n6. Trademarks. This License does not grant permission to use the trade\n names, trademarks, service marks, or product names of the Licensor,\n except as required for reasonable and customary use in describing the\n origin of the Work and reproducing the content of the NOTICE file.\n\n7. Disclaimer of Warranty. Unless required by applicable law or\n agreed to in writing, Licensor provides the Work (and each\n Contributor provides its Contributions) on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n implied, including, without limitation, any warranties or conditions\n of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n PARTICULAR PURPOSE. You are solely responsible for determining the\n appropriateness of using or redistributing the Work and assume any\n risks associated with Your exercise of permissions under this License.\n\n8. Limitation of Liability. In no event and under no legal theory,\n whether in tort (including negligence), contract, or otherwise,\n unless required by applicable law (such as deliberate and grossly\n negligent acts) or agreed to in writing, shall any Contributor be\n liable to You for damages, including any direct, indirect, special,\n incidental, or consequential damages of any character arising as a\n result of this License or out of the use or inability to use the\n Work (including but not limited to damages for loss of goodwill,\n work stoppage, computer failure or malfunction, or any and all\n other commercial damages or losses), even if such Contributor\n has been advised of the possibility of such damages.\n\n9. Accepting Warranty or Additional Liability. While redistributing\n the Work or Derivative Works thereof, You may choose to offer,\n and charge a fee for, acceptance of support, warranty, indemnity,\n or other liability obligations and/or rights consistent with this\n License. However, in accepting such obligations, You may act only\n on Your own behalf and on Your sole responsibility, not on behalf\n of any other Contributor, and only if You agree to indemnify,\n defend, and hold each Contributor harmless for any liability\n incurred by, or claims asserted against, such Contributor by reason\n of your accepting any such warranty or additional liability.\n\nEND OF TERMS AND CONDITIONS\n\nAPPENDIX: How to apply the Apache License to your work.\n\n To apply the Apache License to your work, attach the following\n boilerplate notice, with the fields enclosed by brackets \"[]\"\n replaced with your own identifying information. (Don't include\n the brackets!) The text should be enclosed in the appropriate\n comment syntax for the file format. We also recommend that a\n file or class name and description of purpose be included on the\n same \"printed page\" as the copyright notice for easier\n identification within third-party archives.\n\nCopyright [yyyy] [name of copyright owner]\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.", - "spdxId": "Apache-2.0", - "hash": "196b44647f01b6b79fdfedf9cd2caed7" - }, "9be0c4d7964ad9a68deb2e9706266b8c": { "name": "CDDL + GPLv2 with classpath exception", "url": "https://github.com/javaee/javax.annotation/blob/master/LICENSE", @@ -126,6 +134,12 @@ "url": "https://raw.githubusercontent.com/javaee/javax.annotation/master/LICENSE", "content": "COMMON DEVELOPMENT AND DISTRIBUTION LICENSE (CDDL) Version 1.1\n\n1. Definitions.\n\n 1.1. \"Contributor\" means each individual or entity that creates or\n contributes to the creation of Modifications.\n\n 1.2. \"Contributor Version\" means the combination of the Original\n Software, prior Modifications used by a Contributor (if any), and\n the Modifications made by that particular Contributor.\n\n 1.3. \"Covered Software\" means (a) the Original Software, or (b)\n Modifications, or (c) the combination of files containing Original\n Software with files containing Modifications, in each case including\n portions thereof.\n\n 1.4. \"Executable\" means the Covered Software in any form other than\n Source Code.\n\n 1.5. \"Initial Developer\" means the individual or entity that first\n makes Original Software available under this License.\n\n 1.6. \"Larger Work\" means a work which combines Covered Software or\n portions thereof with code not governed by the terms of this License.\n\n 1.7. \"License\" means this document.\n\n 1.8. \"Licensable\" means having the right to grant, to the maximum\n extent possible, whether at the time of the initial grant or\n subsequently acquired, any and all of the rights conveyed herein.\n\n 1.9. \"Modifications\" means the Source Code and Executable form of\n any of the following:\n\n A. Any file that results from an addition to, deletion from or\n modification of the contents of a file containing Original Software\n or previous Modifications;\n\n B. Any new file that contains any part of the Original Software or\n previous Modification; or\n\n C. Any new file that is contributed or otherwise made available\n under the terms of this License.\n\n 1.10. \"Original Software\" means the Source Code and Executable form\n of computer software code that is originally released under this\n License.\n\n 1.11. \"Patent Claims\" means any patent claim(s), now owned or\n hereafter acquired, including without limitation, method, process,\n and apparatus claims, in any patent Licensable by grantor.\n\n 1.12. \"Source Code\" means (a) the common form of computer software\n code in which modifications are made and (b) associated\n documentation included in or with such code.\n\n 1.13. \"You\" (or \"Your\") means an individual or a legal entity\n exercising rights under, and complying with all of the terms of,\n this License. For legal entities, \"You\" includes any entity which\n controls, is controlled by, or is under common control with You. For\n purposes of this definition, \"control\" means (a) the power, direct\n or indirect, to cause the direction or management of such entity,\n whether by contract or otherwise, or (b) ownership of more than\n fifty percent (50%) of the outstanding shares or beneficial\n ownership of such entity.\n\n2. License Grants.\n\n 2.1. The Initial Developer Grant.\n\n Conditioned upon Your compliance with Section 3.1 below and subject\n to third party intellectual property claims, the Initial Developer\n hereby grants You a world-wide, royalty-free, non-exclusive license:\n\n (a) under intellectual property rights (other than patent or\n trademark) Licensable by Initial Developer, to use, reproduce,\n modify, display, perform, sublicense and distribute the Original\n Software (or portions thereof), with or without Modifications,\n and/or as part of a Larger Work; and\n\n (b) under Patent Claims infringed by the making, using or selling of\n Original Software, to make, have made, use, practice, sell, and\n offer for sale, and/or otherwise dispose of the Original Software\n (or portions thereof).\n\n (c) The licenses granted in Sections 2.1(a) and (b) are effective on\n the date Initial Developer first distributes or otherwise makes the\n Original Software available to a third party under the terms of this\n License.\n\n (d) Notwithstanding Section 2.1(b) above, no patent license is\n granted: (1) for code that You delete from the Original Software, or\n (2) for infringements caused by: (i) the modification of the\n Original Software, or (ii) the combination of the Original Software\n with other software or devices.\n\n 2.2. Contributor Grant.\n\n Conditioned upon Your compliance with Section 3.1 below and subject\n to third party intellectual property claims, each Contributor hereby\n grants You a world-wide, royalty-free, non-exclusive license:\n\n (a) under intellectual property rights (other than patent or\n trademark) Licensable by Contributor to use, reproduce, modify,\n display, perform, sublicense and distribute the Modifications\n created by such Contributor (or portions thereof), either on an\n unmodified basis, with other Modifications, as Covered Software\n and/or as part of a Larger Work; and\n\n (b) under Patent Claims infringed by the making, using, or selling\n of Modifications made by that Contributor either alone and/or in\n combination with its Contributor Version (or portions of such\n combination), to make, use, sell, offer for sale, have made, and/or\n otherwise dispose of: (1) Modifications made by that Contributor (or\n portions thereof); and (2) the combination of Modifications made by\n that Contributor with its Contributor Version (or portions of such\n combination).\n\n (c) The licenses granted in Sections 2.2(a) and 2.2(b) are effective\n on the date Contributor first distributes or otherwise makes the\n Modifications available to a third party.\n\n (d) Notwithstanding Section 2.2(b) above, no patent license is\n granted: (1) for any code that Contributor has deleted from the\n Contributor Version; (2) for infringements caused by: (i) third\n party modifications of Contributor Version, or (ii) the combination\n of Modifications made by that Contributor with other software\n (except as part of the Contributor Version) or other devices; or (3)\n under Patent Claims infringed by Covered Software in the absence of\n Modifications made by that Contributor.\n\n3. Distribution Obligations.\n\n 3.1. Availability of Source Code.\n\n Any Covered Software that You distribute or otherwise make available\n in Executable form must also be made available in Source Code form\n and that Source Code form must be distributed only under the terms\n of this License. You must include a copy of this License with every\n copy of the Source Code form of the Covered Software You distribute\n or otherwise make available. You must inform recipients of any such\n Covered Software in Executable form as to how they can obtain such\n Covered Software in Source Code form in a reasonable manner on or\n through a medium customarily used for software exchange.\n\n 3.2. Modifications.\n\n The Modifications that You create or to which You contribute are\n governed by the terms of this License. You represent that You\n believe Your Modifications are Your original creation(s) and/or You\n have sufficient rights to grant the rights conveyed by this License.\n\n 3.3. Required Notices.\n\n You must include a notice in each of Your Modifications that\n identifies You as the Contributor of the Modification. You may not\n remove or alter any copyright, patent or trademark notices contained\n within the Covered Software, or any notices of licensing or any\n descriptive text giving attribution to any Contributor or the\n Initial Developer.\n\n 3.4. Application of Additional Terms.\n\n You may not offer or impose any terms on any Covered Software in\n Source Code form that alters or restricts the applicable version of\n this License or the recipients' rights hereunder. You may choose to\n offer, and to charge a fee for, warranty, support, indemnity or\n liability obligations to one or more recipients of Covered Software.\n However, you may do so only on Your own behalf, and not on behalf of\n the Initial Developer or any Contributor. You must make it\n absolutely clear that any such warranty, support, indemnity or\n liability obligation is offered by You alone, and You hereby agree\n to indemnify the Initial Developer and every Contributor for any\n liability incurred by the Initial Developer or such Contributor as a\n result of warranty, support, indemnity or liability terms You offer.\n\n 3.5. Distribution of Executable Versions.\n\n You may distribute the Executable form of the Covered Software under\n the terms of this License or under the terms of a license of Your\n choice, which may contain terms different from this License,\n provided that You are in compliance with the terms of this License\n and that the license for the Executable form does not attempt to\n limit or alter the recipient's rights in the Source Code form from\n the rights set forth in this License. If You distribute the Covered\n Software in Executable form under a different license, You must make\n it absolutely clear that any terms which differ from this License\n are offered by You alone, not by the Initial Developer or\n Contributor. You hereby agree to indemnify the Initial Developer and\n every Contributor for any liability incurred by the Initial\n Developer or such Contributor as a result of any such terms You offer.\n\n 3.6. Larger Works.\n\n You may create a Larger Work by combining Covered Software with\n other code not governed by the terms of this License and distribute\n the Larger Work as a single product. In such a case, You must make\n sure the requirements of this License are fulfilled for the Covered\n Software.\n\n4. Versions of the License.\n\n 4.1. New Versions.\n\n Oracle is the initial license steward and may publish revised and/or\n new versions of this License from time to time. Each version will be\n given a distinguishing version number. Except as provided in Section\n 4.3, no one other than the license steward has the right to modify\n this License.\n\n 4.2. Effect of New Versions.\n\n You may always continue to use, distribute or otherwise make the\n Covered Software available under the terms of the version of the\n License under which You originally received the Covered Software. If\n the Initial Developer includes a notice in the Original Software\n prohibiting it from being distributed or otherwise made available\n under any subsequent version of the License, You must distribute and\n make the Covered Software available under the terms of the version\n of the License under which You originally received the Covered\n Software. Otherwise, You may also choose to use, distribute or\n otherwise make the Covered Software available under the terms of any\n subsequent version of the License published by the license steward.\n\n 4.3. Modified Versions.\n\n When You are an Initial Developer and You want to create a new\n license for Your Original Software, You may create and use a\n modified version of this License if You: (a) rename the license and\n remove any references to the name of the license steward (except to\n note that the license differs from this License); and (b) otherwise\n make it clear that the license contains terms which differ from this\n License.\n\n5. DISCLAIMER OF WARRANTY.\n\n COVERED SOFTWARE IS PROVIDED UNDER THIS LICENSE ON AN \"AS IS\" BASIS,\n WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED,\n INCLUDING, WITHOUT LIMITATION, WARRANTIES THAT THE COVERED SOFTWARE\n IS FREE OF DEFECTS, MERCHANTABLE, FIT FOR A PARTICULAR PURPOSE OR\n NON-INFRINGING. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF\n THE COVERED SOFTWARE IS WITH YOU. SHOULD ANY COVERED SOFTWARE PROVE\n DEFECTIVE IN ANY RESPECT, YOU (NOT THE INITIAL DEVELOPER OR ANY\n OTHER CONTRIBUTOR) ASSUME THE COST OF ANY NECESSARY SERVICING,\n REPAIR OR CORRECTION. THIS DISCLAIMER OF WARRANTY CONSTITUTES AN\n ESSENTIAL PART OF THIS LICENSE. NO USE OF ANY COVERED SOFTWARE IS\n AUTHORIZED HEREUNDER EXCEPT UNDER THIS DISCLAIMER.\n\n6. TERMINATION.\n\n 6.1. This License and the rights granted hereunder will terminate\n automatically if You fail to comply with terms herein and fail to\n cure such breach within 30 days of becoming aware of the breach.\n Provisions which, by their nature, must remain in effect beyond the\n termination of this License shall survive.\n\n 6.2. If You assert a patent infringement claim (excluding\n declaratory judgment actions) against Initial Developer or a\n Contributor (the Initial Developer or Contributor against whom You\n assert such claim is referred to as \"Participant\") alleging that the\n Participant Software (meaning the Contributor Version where the\n Participant is a Contributor or the Original Software where the\n Participant is the Initial Developer) directly or indirectly\n infringes any patent, then any and all rights granted directly or\n indirectly to You by such Participant, the Initial Developer (if the\n Initial Developer is not the Participant) and all Contributors under\n Sections 2.1 and/or 2.2 of this License shall, upon 60 days notice\n from Participant terminate prospectively and automatically at the\n expiration of such 60 day notice period, unless if within such 60\n day period You withdraw Your claim with respect to the Participant\n Software against such Participant either unilaterally or pursuant to\n a written agreement with Participant.\n\n 6.3. If You assert a patent infringement claim against Participant\n alleging that the Participant Software directly or indirectly\n infringes any patent where such claim is resolved (such as by\n license or settlement) prior to the initiation of patent\n infringement litigation, then the reasonable value of the licenses\n granted by such Participant under Sections 2.1 or 2.2 shall be taken\n into account in determining the amount or value of any payment or\n license.\n\n 6.4. In the event of termination under Sections 6.1 or 6.2 above,\n all end user licenses that have been validly granted by You or any\n distributor hereunder prior to termination (excluding licenses\n granted to You by any distributor) shall survive termination.\n\n7. LIMITATION OF LIABILITY.\n\n UNDER NO CIRCUMSTANCES AND UNDER NO LEGAL THEORY, WHETHER TORT\n (INCLUDING NEGLIGENCE), CONTRACT, OR OTHERWISE, SHALL YOU, THE\n INITIAL DEVELOPER, ANY OTHER CONTRIBUTOR, OR ANY DISTRIBUTOR OF\n COVERED SOFTWARE, OR ANY SUPPLIER OF ANY OF SUCH PARTIES, BE LIABLE\n TO ANY PERSON FOR ANY INDIRECT, SPECIAL, INCIDENTAL, OR\n CONSEQUENTIAL DAMAGES OF ANY CHARACTER INCLUDING, WITHOUT\n LIMITATION, DAMAGES FOR LOSS OF GOODWILL, WORK STOPPAGE, COMPUTER\n FAILURE OR MALFUNCTION, OR ANY AND ALL OTHER COMMERCIAL DAMAGES OR\n LOSSES, EVEN IF SUCH PARTY SHALL HAVE BEEN INFORMED OF THE\n POSSIBILITY OF SUCH DAMAGES. THIS LIMITATION OF LIABILITY SHALL NOT\n APPLY TO LIABILITY FOR DEATH OR PERSONAL INJURY RESULTING FROM SUCH\n PARTY'S NEGLIGENCE TO THE EXTENT APPLICABLE LAW PROHIBITS SUCH\n LIMITATION. SOME JURISDICTIONS DO NOT ALLOW THE EXCLUSION OR\n LIMITATION OF INCIDENTAL OR CONSEQUENTIAL DAMAGES, SO THIS EXCLUSION\n AND LIMITATION MAY NOT APPLY TO YOU.\n\n8. U.S. GOVERNMENT END USERS.\n\n The Covered Software is a \"commercial item,\" as that term is defined\n in 48 C.F.R. 2.101 (Oct. 1995), consisting of \"commercial computer\n software\" (as that term is defined at 48 C.F.R. \u00a7\n 252.227-7014(a)(1)) and \"commercial computer software documentation\"\n as such terms are used in 48 C.F.R. 12.212 (Sept. 1995). Consistent\n with 48 C.F.R. 12.212 and 48 C.F.R. 227.7202-1 through 227.7202-4\n (June 1995), all U.S. Government End Users acquire Covered Software\n with only those rights set forth herein. This U.S. Government Rights\n clause is in lieu of, and supersedes, any other FAR, DFAR, or other\n clause or provision that addresses Government rights in computer\n software under this License.\n\n9. MISCELLANEOUS.\n\n This License represents the complete agreement concerning subject\n matter hereof. If any provision of this License is held to be\n unenforceable, such provision shall be reformed only to the extent\n necessary to make it enforceable. This License shall be governed by\n the law of the jurisdiction specified in a notice contained within\n the Original Software (except to the extent applicable law, if any,\n provides otherwise), excluding such jurisdiction's conflict-of-law\n provisions. Any litigation relating to this License shall be subject\n to the jurisdiction of the courts located in the jurisdiction and\n venue specified in a notice contained within the Original Software,\n with the losing party responsible for costs, including, without\n limitation, court costs and reasonable attorneys' fees and expenses.\n The application of the United Nations Convention on Contracts for\n the International Sale of Goods is expressly excluded. Any law or\n regulation which provides that the language of a contract shall be\n construed against the drafter shall not apply to this License. You\n agree that You alone are responsible for compliance with the United\n States export administration regulations (and the export control\n laws and regulation of any other countries) when You use, distribute\n or otherwise make available any Covered Software.\n\n10. RESPONSIBILITY FOR CLAIMS.\n\n As between Initial Developer and the Contributors, each party is\n responsible for claims and damages arising, directly or indirectly,\n out of its utilization of rights under this License and You agree to\n work with Initial Developer and Contributors to distribute such\n responsibility on an equitable basis. Nothing herein is intended or\n shall be deemed to constitute any admission of liability.\n\n------------------------------------------------------------------------\n\nNOTICE PURSUANT TO SECTION 9 OF THE COMMON DEVELOPMENT AND DISTRIBUTION\nLICENSE (CDDL)\n\nThe code released under the CDDL shall be governed by the laws of the\nState of California (excluding conflict-of-law provisions). Any\nlitigation relating to this License shall be subject to the jurisdiction\nof the Federal Courts of the Northern District of California and the\nstate courts of the State of California, with venue lying in Santa Clara\nCounty, California.\n\n\n\n The GNU General Public License (GPL) Version 2, June 1991\n\nCopyright (C) 1989, 1991 Free Software Foundation, Inc.\n51 Franklin Street, Fifth Floor\nBoston, MA 02110-1335\nUSA\n\nEveryone is permitted to copy and distribute verbatim copies\nof this license document, but changing it is not allowed.\n\nPreamble\n\nThe licenses for most software are designed to take away your freedom to\nshare and change it. By contrast, the GNU General Public License is\nintended to guarantee your freedom to share and change free software--to\nmake sure the software is free for all its users. This General Public\nLicense applies to most of the Free Software Foundation's software and\nto any other program whose authors commit to using it. (Some other Free\nSoftware Foundation software is covered by the GNU Library General\nPublic License instead.) You can apply it to your programs, too.\n\nWhen we speak of free software, we are referring to freedom, not price.\nOur General Public Licenses are designed to make sure that you have the\nfreedom to distribute copies of free software (and charge for this\nservice if you wish), that you receive source code or can get it if you\nwant it, that you can change the software or use pieces of it in new\nfree programs; and that you know you can do these things.\n\nTo protect your rights, we need to make restrictions that forbid anyone\nto deny you these rights or to ask you to surrender the rights. These\nrestrictions translate to certain responsibilities for you if you\ndistribute copies of the software, or if you modify it.\n\nFor example, if you distribute copies of such a program, whether gratis\nor for a fee, you must give the recipients all the rights that you have.\nYou must make sure that they, too, receive or can get the source code.\nAnd you must show them these terms so they know their rights.\n\nWe protect your rights with two steps: (1) copyright the software, and\n(2) offer you this license which gives you legal permission to copy,\ndistribute and/or modify the software.\n\nAlso, for each author's protection and ours, we want to make certain\nthat everyone understands that there is no warranty for this free\nsoftware. If the software is modified by someone else and passed on, we\nwant its recipients to know that what they have is not the original, so\nthat any problems introduced by others will not reflect on the original\nauthors' reputations.\n\nFinally, any free program is threatened constantly by software patents.\nWe wish to avoid the danger that redistributors of a free program will\nindividually obtain patent licenses, in effect making the program\nproprietary. To prevent this, we have made it clear that any patent must\nbe licensed for everyone's free use or not licensed at all.\n\nThe precise terms and conditions for copying, distribution and\nmodification follow.\n\nTERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION\n\n0. This License applies to any program or other work which contains a\nnotice placed by the copyright holder saying it may be distributed under\nthe terms of this General Public License. The \"Program\", below, refers\nto any such program or work, and a \"work based on the Program\" means\neither the Program or any derivative work under copyright law: that is\nto say, a work containing the Program or a portion of it, either\nverbatim or with modifications and/or translated into another language.\n(Hereinafter, translation is included without limitation in the term\n\"modification\".) Each licensee is addressed as \"you\".\n\nActivities other than copying, distribution and modification are not\ncovered by this License; they are outside its scope. The act of running\nthe Program is not restricted, and the output from the Program is\ncovered only if its contents constitute a work based on the Program\n(independent of having been made by running the Program). Whether that\nis true depends on what the Program does.\n\n1. You may copy and distribute verbatim copies of the Program's source\ncode as you receive it, in any medium, provided that you conspicuously\nand appropriately publish on each copy an appropriate copyright notice\nand disclaimer of warranty; keep intact all the notices that refer to\nthis License and to the absence of any warranty; and give any other\nrecipients of the Program a copy of this License along with the Program.\n\nYou may charge a fee for the physical act of transferring a copy, and\nyou may at your option offer warranty protection in exchange for a fee.\n\n2. You may modify your copy or copies of the Program or any portion of\nit, thus forming a work based on the Program, and copy and distribute\nsuch modifications or work under the terms of Section 1 above, provided\nthat you also meet all of these conditions:\n\n a) You must cause the modified files to carry prominent notices\n stating that you changed the files and the date of any change.\n\n b) You must cause any work that you distribute or publish, that in\n whole or in part contains or is derived from the Program or any part\n thereof, to be licensed as a whole at no charge to all third parties\n under the terms of this License.\n\n c) If the modified program normally reads commands interactively\n when run, you must cause it, when started running for such\n interactive use in the most ordinary way, to print or display an\n announcement including an appropriate copyright notice and a notice\n that there is no warranty (or else, saying that you provide a\n warranty) and that users may redistribute the program under these\n conditions, and telling the user how to view a copy of this License.\n (Exception: if the Program itself is interactive but does not\n normally print such an announcement, your work based on the Program\n is not required to print an announcement.)\n\nThese requirements apply to the modified work as a whole. If\nidentifiable sections of that work are not derived from the Program, and\ncan be reasonably considered independent and separate works in\nthemselves, then this License, and its terms, do not apply to those\nsections when you distribute them as separate works. But when you\ndistribute the same sections as part of a whole which is a work based on\nthe Program, the distribution of the whole must be on the terms of this\nLicense, whose permissions for other licensees extend to the entire\nwhole, and thus to each and every part regardless of who wrote it.\n\nThus, it is not the intent of this section to claim rights or contest\nyour rights to work written entirely by you; rather, the intent is to\nexercise the right to control the distribution of derivative or\ncollective works based on the Program.\n\nIn addition, mere aggregation of another work not based on the Program\nwith the Program (or with a work based on the Program) on a volume of a\nstorage or distribution medium does not bring the other work under the\nscope of this License.\n\n3. You may copy and distribute the Program (or a work based on it,\nunder Section 2) in object code or executable form under the terms of\nSections 1 and 2 above provided that you also do one of the following:\n\n a) Accompany it with the complete corresponding machine-readable\n source code, which must be distributed under the terms of Sections 1\n and 2 above on a medium customarily used for software interchange; or,\n\n b) Accompany it with a written offer, valid for at least three\n years, to give any third party, for a charge no more than your cost\n of physically performing source distribution, a complete\n machine-readable copy of the corresponding source code, to be\n distributed under the terms of Sections 1 and 2 above on a medium\n customarily used for software interchange; or,\n\n c) Accompany it with the information you received as to the offer to\n distribute corresponding source code. (This alternative is allowed\n only for noncommercial distribution and only if you received the\n program in object code or executable form with such an offer, in\n accord with Subsection b above.)\n\nThe source code for a work means the preferred form of the work for\nmaking modifications to it. For an executable work, complete source code\nmeans all the source code for all modules it contains, plus any\nassociated interface definition files, plus the scripts used to control\ncompilation and installation of the executable. However, as a special\nexception, the source code distributed need not include anything that is\nnormally distributed (in either source or binary form) with the major\ncomponents (compiler, kernel, and so on) of the operating system on\nwhich the executable runs, unless that component itself accompanies the\nexecutable.\n\nIf distribution of executable or object code is made by offering access\nto copy from a designated place, then offering equivalent access to copy\nthe source code from the same place counts as distribution of the source\ncode, even though third parties are not compelled to copy the source\nalong with the object code.\n\n4. You may not copy, modify, sublicense, or distribute the Program\nexcept as expressly provided under this License. Any attempt otherwise\nto copy, modify, sublicense or distribute the Program is void, and will\nautomatically terminate your rights under this License. However, parties\nwho have received copies, or rights, from you under this License will\nnot have their licenses terminated so long as such parties remain in\nfull compliance.\n\n5. You are not required to accept this License, since you have not\nsigned it. However, nothing else grants you permission to modify or\ndistribute the Program or its derivative works. These actions are\nprohibited by law if you do not accept this License. Therefore, by\nmodifying or distributing the Program (or any work based on the\nProgram), you indicate your acceptance of this License to do so, and all\nits terms and conditions for copying, distributing or modifying the\nProgram or works based on it.\n\n6. Each time you redistribute the Program (or any work based on the\nProgram), the recipient automatically receives a license from the\noriginal licensor to copy, distribute or modify the Program subject to\nthese terms and conditions. You may not impose any further restrictions\non the recipients' exercise of the rights granted herein. You are not\nresponsible for enforcing compliance by third parties to this License.\n\n7. If, as a consequence of a court judgment or allegation of patent\ninfringement or for any other reason (not limited to patent issues),\nconditions are imposed on you (whether by court order, agreement or\notherwise) that contradict the conditions of this License, they do not\nexcuse you from the conditions of this License. If you cannot distribute\nso as to satisfy simultaneously your obligations under this License and\nany other pertinent obligations, then as a consequence you may not\ndistribute the Program at all. For example, if a patent license would\nnot permit royalty-free redistribution of the Program by all those who\nreceive copies directly or indirectly through you, then the only way you\ncould satisfy both it and this License would be to refrain entirely from\ndistribution of the Program.\n\nIf any portion of this section is held invalid or unenforceable under\nany particular circumstance, the balance of the section is intended to\napply and the section as a whole is intended to apply in other\ncircumstances.\n\nIt is not the purpose of this section to induce you to infringe any\npatents or other property right claims or to contest validity of any\nsuch claims; this section has the sole purpose of protecting the\nintegrity of the free software distribution system, which is implemented\nby public license practices. Many people have made generous\ncontributions to the wide range of software distributed through that\nsystem in reliance on consistent application of that system; it is up to\nthe author/donor to decide if he or she is willing to distribute\nsoftware through any other system and a licensee cannot impose that choice.\n\nThis section is intended to make thoroughly clear what is believed to be\na consequence of the rest of this License.\n\n8. If the distribution and/or use of the Program is restricted in\ncertain countries either by patents or by copyrighted interfaces, the\noriginal copyright holder who places the Program under this License may\nadd an explicit geographical distribution limitation excluding those\ncountries, so that distribution is permitted only in or among countries\nnot thus excluded. In such case, this License incorporates the\nlimitation as if written in the body of this License.\n\n9. The Free Software Foundation may publish revised and/or new\nversions of the General Public License from time to time. Such new\nversions will be similar in spirit to the present version, but may\ndiffer in detail to address new problems or concerns.\n\nEach version is given a distinguishing version number. If the Program\nspecifies a version number of this License which applies to it and \"any\nlater version\", you have the option of following the terms and\nconditions either of that version or of any later version published by\nthe Free Software Foundation. If the Program does not specify a version\nnumber of this License, you may choose any version ever published by the\nFree Software Foundation.\n\n10. If you wish to incorporate parts of the Program into other free\nprograms whose distribution conditions are different, write to the\nauthor to ask for permission. For software which is copyrighted by the\nFree Software Foundation, write to the Free Software Foundation; we\nsometimes make exceptions for this. Our decision will be guided by the\ntwo goals of preserving the free status of all derivatives of our free\nsoftware and of promoting the sharing and reuse of software generally.\n\nNO WARRANTY\n\n11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO\nWARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW.\nEXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR\nOTHER PARTIES PROVIDE THE PROGRAM \"AS IS\" WITHOUT WARRANTY OF ANY KIND,\nEITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED\nWARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE\nENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH\nYOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL\nNECESSARY SERVICING, REPAIR OR CORRECTION.\n\n12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN\nWRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY\nAND/OR REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR\nDAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL\nDAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM\n(INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED\nINACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF\nTHE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR\nOTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.\n\nEND OF TERMS AND CONDITIONS\n\nHow to Apply These Terms to Your New Programs\n\nIf you develop a new program, and you want it to be of the greatest\npossible use to the public, the best way to achieve this is to make it\nfree software which everyone can redistribute and change under these terms.\n\nTo do so, attach the following notices to the program. It is safest to\nattach them to the start of each source file to most effectively convey\nthe exclusion of warranty; and each file should have at least the\n\"copyright\" line and a pointer to where the full notice is found.\n\n One line to give the program's name and a brief idea of what it does.\n Copyright (C) \n\n This program is free software; you can redistribute it and/or modify\n it under the terms of the GNU General Public License as published by\n the Free Software Foundation; either version 2 of the License, or\n (at your option) any later version.\n\n This program is distributed in the hope that it will be useful, but\n WITHOUT ANY WARRANTY; without even the implied warranty of\n MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU\n General Public License for more details.\n\n You should have received a copy of the GNU General Public License\n along with this program; if not, write to the Free Software\n Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335 USA\n\nAlso add information on how to contact you by electronic and paper mail.\n\nIf the program is interactive, make it output a short notice like this\nwhen it starts in an interactive mode:\n\n Gnomovision version 69, Copyright (C) year name of author\n Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type\n `show w'. This is free software, and you are welcome to redistribute\n it under certain conditions; type `show c' for details.\n\nThe hypothetical commands `show w' and `show c' should show the\nappropriate parts of the General Public License. Of course, the commands\nyou use may be called something other than `show w' and `show c'; they\ncould even be mouse-clicks or menu items--whatever suits your program.\n\nYou should also get your employer (if you work as a programmer) or your\nschool, if any, to sign a \"copyright disclaimer\" for the program, if\nnecessary. Here is a sample; alter the names:\n\n Yoyodyne, Inc., hereby disclaims all copyright interest in the\n program `Gnomovision' (which makes passes at compilers) written by\n James Hacker.\n\n signature of Ty Coon, 1 April 1989\n Ty Coon, President of Vice\n\nThis General Public License does not permit incorporating your program\ninto proprietary programs. If your program is a subroutine library, you\nmay consider it more useful to permit linking proprietary applications\nwith the library. If this is what you want to do, use the GNU Library\nGeneral Public License instead of this License.\n\n#\n\nCertain source files distributed by Oracle America, Inc. and/or its\naffiliates are subject to the following clarification and special\nexception to the GPLv2, based on the GNU Project exception for its\nClasspath libraries, known as the GNU Classpath Exception, but only\nwhere Oracle has expressly included in the particular source file's\nheader the words \"Oracle designates this particular file as subject to\nthe \"Classpath\" exception as provided by Oracle in the LICENSE file\nthat accompanied this code.\"\n\nYou should also note that Oracle includes multiple, independent\nprograms in this software package. Some of those programs are provided\nunder licenses deemed incompatible with the GPLv2 by the Free Software\nFoundation and others. For example, the package includes programs\nlicensed under the Apache License, Version 2.0. Such programs are\nlicensed to you under their original licenses.\n\nOracle facilitates your further distribution of this package by adding\nthe Classpath Exception to the necessary parts of its GPLv2 code, which\npermits you to use that code in combination with other independent\nmodules not licensed under the GPLv2. However, note that this would\nnot permit you to commingle code under an incompatible license with\nOracle's GPLv2 licensed code by, for example, cutting and pasting such\ncode into a file also containing Oracle's GPLv2 licensed code and then\ndistributing the result. Additionally, if you were to remove the\nClasspath Exception from any of the files to which it applies and\ndistribute the result, you would likely be required to license some or\nall of the other code in that distribution under the GPLv2 as well, and\nsince the GPLv2 is incompatible with the license terms of some items\nincluded in the distribution by Oracle, removing the Classpath\nException could therefore effectively compromise your ability to\nfurther distribute the package.\n\nProceed with caution and we recommend that you obtain the advice of a\nlawyer skilled in open source matters before removing the Classpath\nException or making modifications to this package which may\nsubsequently be redistributed and/or involve the use of third party\nsoftware.\n\nCLASSPATH EXCEPTION\nLinking this library statically or dynamically with other modules is\nmaking a combined work based on this library. Thus, the terms and\nconditions of the GNU General Public License version 2 cover the whole\ncombination.\n\nAs a special exception, the copyright holders of this library give you\npermission to link this library with independent modules to produce an\nexecutable, regardless of the license terms of these independent\nmodules, and to copy and distribute the resulting executable under\nterms of your choice, provided that you also meet, for each linked\nindependent module, the terms and conditions of the license of that\nmodule. An independent module is a module which is not derived from or\nbased on this library. If you modify this library, you may extend this\nexception to your version of the library, but you are not obligated to\ndo so. If you do not wish to do so, delete this exception statement\nfrom your version.", "hash": "e1692074a62fa0fd6ef3ef00ec4904f0" + }, + "fea9e903303ed8cbc7854c24956a8913": { + "name": "Other", + "url": "https://raw.githubusercontent.com/JetBrains/intellij-community/master/LICENSE.txt", + "content": "JETBRAINS OPEN-SOURCE BUILD TERMS\n\nVersion 1.3, effective as of June 15, 2026\n\nIMPORTANT! READ CAREFULLY:\n\nTHESE TERMS APPLY TO THE OPEN-SOURCE BUILDS OF THE JETBRAINS INTEGRATED DEVELOPMENT ENVIRONMENT TOOLS\nCALLED \u2018INTELLIJ IDEA\u2019 AND \u2018PYCHARM\u2019 (SUCH TOOLS, \u201cOPEN-SOURCE BUILD\u201d PRODUCTS) WHICH CONSIST OF OPEN SOURCE SOFTWARE\nSUBJECT TO THE APACHE 2.0 LICENSE (AVAILABLE HERE: https://www.apache.org/licenses/LICENSE-2.0).\n\n\"JetBrains\" or \"we\" means JetBrains s.r.o., with its principal place of business\nat Na Hrebenech II 1718/8, Prague, 14000, Czech Republic, registered in the Commercial Register\nmaintained by the Municipal Court of Prague, Section C, File 86211, ID No.: 265 02 275.\n\n\"You\" means any Organization or natural person using an Open-Source Build product in accordance with these terms,\nwhere \u201cOrganization\u201d includes any corporation, company, partnership, association, or other entity that controls,\nis controlled by, or is under common control with you. For the purposes of this definition, \u201ccontrol\u201d means\n(i) the power, directly or indirectly, to direct or manage such entity, whether by contract or otherwise, or\n(ii) ownership of fifty percent (50%) or more of the outstanding shares or beneficial ownership of such entity.\n\nPersonal Data \u2014 In connection with your use of Open-Source Build products, we and our associated companies will\nprocess your personal data, including but not limited to your contact and identification details, and data about\nyour usage of our software and services, in order to (i) provide you with software, services, or information;\n(ii) protect us from piracy and unlawful use of our software or services; (iii) improve our offerings based on usage;\n(iv) create and maintain our internal records and to protect our rights and interests and those of other users;\n(v) promote and market our software and services; and\n(vi) to fulfil legal duties stipulated by accounting, taxation, and other laws. You may object to the processing\nof your personal data for the purposes of (i) through (v) at any time. More detailed information can be found in\nour Privacy Notice, available here: https://www.jetbrains.com/legal/docs/privacy/privacy.html.\n\nPersonal Data Collected \u2014 For the above purposes, we may collect, among other things, your IP address,\nJetBrains Account username, JetBrains Account password, first name, last name, and email address.\n\nAnonymous Data \u2014 On installation and execution, the Open-Source Build product may send us certain information,\nwhich will not contain any personal data, including product version, product edition, and information about\nthe operating system where the Open-Source Build product is installed. A unique ID, which\ndoes not contain any personal data, is also used to distinguish instances. The Open-Source Build product\ncan check for available updates, as well as available updates for plugins or components.\nIn addition, you can opt in to further anonymous data processing. If you do so, the Open-Source Build product\nmay electronically send anonymous information to us related to your usage of the product features.\nThis further information may include, but is not limited to, frameworks, file templates being used in the IDEs,\nactions invoked, and other interactions with product features. This information will not contain your source code,\nyour personal data, information about your JetBrains Account, or subscription information.\n\nAccidentally Sent Information \u2014 We are not responsible for any processing of personal data you accidentally send to us.\n\nFeedback \u2014 You have no obligation to provide us with ideas, guidance, suggestions, proposals, or bug reports (\u201cFeedback\u201d).\nHowever, if you submit Feedback to us, then you grant us a non-exclusive, worldwide, royalty-free license that is\nsub-licensable and transferable, to make, use, sell, have made, offer to sell, import, reproduce, publicly display,\ndistribute, modify, or publicly perform the Feedback in any manner without any obligation, royalty, or restriction\nbased on intellectual property rights or otherwise.\n\nThird Party Software \u2014 Open-Source Build includes code and libraries licensed to us by third parties, including\nopen source software (\u201cThird-Party Software\u201d). A list of Third-Party Software included in each Open-Source Build product\nis available in the product documentation. All Third-Party Software is provided to you under the respective terms\nstipulated in the Open-Source Build product documentation.\n\nDISCLAIMER OF DAMAGES AND EXCLUSION OF LIABILITY \u2014 ALL OPEN-SOURCE BUILD PRODUCTS (INCLUDING THIRD-PARTY SOFTWARE)\nARE PROVIDED TO YOU ON AN \u201cAS IS\u201d AND \u201cAS AVAILABLE\u201d BASIS. USE OF THE OPEN-SOURCE BUILD PRODUCTS IS AT YOUR OWN RISK\nAND WITHOUT WARRANTIES, EXPRESS OR IMPLIED (TO THE MAXIMUM EXTENT PERMITTED BY LAW). FURTHER DETAILS MAY BE FOUND\nIN THE APPLICABLE OPEN-SOURCE BUILD TERMS OR THIRD-PARTY SOFTWARE TERMS. IN NO EVENT WILL WE BE LIABLE TO YOU\nOR ANYONE ELSE FOR ANY LOSS OF USE, DATA, GOODWILL, OR PROFITS, WHETHER OR NOT FORESEEABLE, OR ANY SPECIAL,\nINCIDENTAL, INDIRECT, CONSEQUENTIAL, EXEMPLARY, OR PUNITIVE DAMAGES OR LOSSES WHATSOEVER INCURRED IN CONNECTION\nWITH YOUR USAGE OF ANY OPEN-SOURCE BUILD PRODUCT. THIS LIMITATION WILL APPLY EVEN IF THE JETBRAINS PARTIES\nHAVE BEEN ADVISED OF THE POSSIBILITY OF LIABILITY.\n\nExport Compliance \u2014 You must comply with all applicable laws and regulations with regard to economic sanctions,\nexport controls, import regulations, restrictive measures, and trade embargoes, including those of the European Union\nand the United States of America.\n\nReservation of Rights \u2014 We reserve the right at any time to cease provision of or alter features, specifications,\ncapabilities, functions, terms of use (including this document), release dates, general availability,\nor other characteristics of Open-Source Build products.\n\nGoverning Law \u2014 These terms are governed by the laws of the Czech Republic, without reference to conflict of laws principles,\nand specifically excluding the United Nations Convention on Contracts for the International Sale of Goods.\nThe parties to this Agreement undertake to use the best commercial efforts to amicably settle any disputes\narising hereunder (\u201cDispute\u201d). Should the parties to this Agreement fail to settle a Dispute amicably, the Dispute\nwill be excluded from the jurisdiction of general courts and all such Disputes will be finally decided\nby the Arbitration Court attached to the Czech Chamber of Commerce and the Agricultural Chamber of the Czech Republic\nby three arbitrators in accordance with the Rules of that Arbitration Court, and the language of the proceedings\nwill be English; provided that if you are a consumer, you and JetBrains agree that any Dispute-related litigation\nmay only be brought in, and shall be subject to the jurisdiction of, any competent court of the Czech Republic,\nunless provided otherwise by applicable consumer law.\nConsumer Disputes can also be settled out of court through the Czech Trade Inspection Authority (www.coi.cz)\nor the European Commission online platform for dispute resolution (ec.europa.eu/consumers/odr).\n\nOpportunity to review \u2014 You declare that you have had sufficient opportunity to review these terms,\nunderstand the content of all of its clauses, negotiate its terms, and seek independent professional legal advice\nin that respect before entering into it. Consequently, any statutory \u201cform contract\u201d (\u201cadhesion contract\u201d) regulations\nshall not be applicable to these terms.", + "hash": "fea9e903303ed8cbc7854c24956a8913" } } } \ No newline at end of file diff --git a/gradle.properties b/gradle.properties index 0b81da240..0e4b16278 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,7 +1,7 @@ GROUP=com.mikepenz -VERSION_NAME=15.0.4 -VERSION_CODE=150004 +VERSION_NAME=15.1.0-b01 +VERSION_CODE=150100 POM_URL=https://github.com/mikepenz/AboutLibraries POM_SCM_URL=https://github.com/mikepenz/AboutLibraries diff --git a/plugin-build/plugin/src/main/kotlin/com/mikepenz/aboutlibraries/plugin/AboutLibrariesExtension.kt b/plugin-build/plugin/src/main/kotlin/com/mikepenz/aboutlibraries/plugin/AboutLibrariesExtension.kt index 446829716..fe39c6d7c 100644 --- a/plugin-build/plugin/src/main/kotlin/com/mikepenz/aboutlibraries/plugin/AboutLibrariesExtension.kt +++ b/plugin-build/plugin/src/main/kotlin/com/mikepenz/aboutlibraries/plugin/AboutLibrariesExtension.kt @@ -75,6 +75,7 @@ abstract class AboutLibrariesExtension { it.fetchRemoteLicense.convention(false) it.fetchRemoteFunding.convention(false) it.filterVariants.convention(emptySet()) + it.includeTargets.convention(false) } export { // it.variant.convention("") (mp) intentionally not set, we use it as `orNull` in places @@ -87,6 +88,7 @@ abstract class AboutLibrariesExtension { it.exclusionPatterns.convention(emptySet()) it.duplicationMode.convention(DuplicateMode.MERGE) it.duplicationRule.convention(DuplicateRule.EXACT) + it.mergePlatformArtifacts.convention(false) } license { it.mapLicensesToSpdx.convention(true) @@ -237,6 +239,34 @@ abstract class CollectorConfig @Inject constructor() { */ @get:Optional abstract val filterVariants: SetProperty + + /** + * Includes the Kotlin targets a library is consumed by, as a `targets` array on each library + * in the generated metadata file (e.g. `["android", "jvm", "iosX64"]`). + * + * The reported values are Kotlin target names as declared in the `kotlin { }` block, resolved + * from the Kotlin target model rather than from configuration names — a configuration is + * attributed to the target whose compilation declares it as its compile or runtime classpath. + * + * Lets a consumer narrow the rendered list to what the running target actually links against, + * e.g. `libs.libraries.filter { "iosArm64" in it.targets }`. + * + * Only multiplatform projects report anything: an Android-only, JVM-only or `java-library` + * project builds a single implicit target, so every library reports an empty `targets` array. + * Use `export.variant` to split an Android build by build variant instead. + * + * Disabled by default. If disabled, the `targets` field is omitted from the output entirely. + * + * ``` + * aboutLibraries { + * collect { + * includeTargets = true + * } + * } + * ``` + */ + @get:Optional + abstract val includeTargets: Property } abstract class ExportConfig @Inject constructor(val name: String = "") { @@ -422,6 +452,47 @@ abstract class LibraryConfig @Inject constructor() { */ @get:Optional abstract val duplicationRule: Property + + /** + * Reports the per-platform artifacts of a Kotlin Multiplatform publication under the single + * root coordinate they were resolved through, instead of under the resolved artifact's own id. + * + * A dependency declared as `com.mikepenz:aboutlibraries-compose-core` resolves to + * `com.mikepenz:aboutlibraries-compose-core-android` on Android. With this enabled the reported + * `uniqueId` is `com.mikepenz:aboutlibraries-compose-core` again, matching what the build script + * declares — which keeps `config` overrides and funding mappings keyed on the declared id. + * + * Note this is *not* about Android build variants — see [filterVariants] and `export.variant` + * for those. The unit here is a Gradle module variant, one per Kotlin target. + * + * Only Gradle `available-at` redirects are collapsed: no coordinate is rewritten unless the + * redirecting root module is itself part of the resolved graph. A suffixed coordinate with no + * such root module (e.g. `androidx.annotation:annotation-jvm` in a graph that never resolves + * `androidx.annotation:annotation`) is untouched — but once the root module is present, the + * platform artifact is reported under it even where it was declared directly. + * Metadata (name, description, licenses) still comes from the resolved artifact. + * + * This is independent of [duplicationMode] / [duplicationRule] and applied before them. Note + * that the default [DuplicateMode.MERGE] already collapses these artifacts onto *one* entry — + * but onto whichever the graph walk reached first, so the surviving id may be a platform one + * (`collection-jvm`) even though the entry stands for every platform. Enabling this makes the + * survivor deterministic and names it after what the build script declared. + * + * Pairs with `collect.includeTargets`: the merged entry reports the union of the targets its + * artifacts were consumed by. + * + * ``` + * aboutLibraries { + * library { + * mergePlatformArtifacts = true + * } + * } + * ``` + * + * @see duplicationMode + */ + @get:Optional + abstract val mergePlatformArtifacts: Property } abstract class LicenseConfig @Inject constructor() { diff --git a/plugin-build/plugin/src/main/kotlin/com/mikepenz/aboutlibraries/plugin/BaseAboutLibrariesTask.kt b/plugin-build/plugin/src/main/kotlin/com/mikepenz/aboutlibraries/plugin/BaseAboutLibrariesTask.kt index a9e7b6ca3..e4c601703 100644 --- a/plugin-build/plugin/src/main/kotlin/com/mikepenz/aboutlibraries/plugin/BaseAboutLibrariesTask.kt +++ b/plugin-build/plugin/src/main/kotlin/com/mikepenz/aboutlibraries/plugin/BaseAboutLibrariesTask.kt @@ -14,6 +14,7 @@ import org.gradle.api.artifacts.component.ModuleComponentIdentifier import org.gradle.api.artifacts.dsl.DependencyHandler import org.gradle.api.file.ConfigurableFileCollection import org.gradle.api.file.DirectoryProperty +import org.gradle.api.plugins.ExtensionContainer import org.gradle.api.provider.ListProperty import org.gradle.api.provider.MapProperty import org.gradle.api.provider.Property @@ -27,6 +28,8 @@ import org.gradle.api.tasks.Optional import org.gradle.api.tasks.PathSensitive import org.gradle.api.tasks.PathSensitivity import org.gradle.work.DisableCachingByDefault +import org.jetbrains.kotlin.gradle.dsl.KotlinSingleTargetExtension +import org.jetbrains.kotlin.gradle.plugin.KotlinTargetsContainer import org.slf4j.LoggerFactory import java.io.File import java.util.regex.Pattern @@ -56,6 +59,9 @@ abstract class BaseAboutLibrariesTask : DefaultTask() { @Input val filterVariants = extension.collect.filterVariants + @Input + val includeTargets = extension.collect.includeTargets + @get:Optional @get:Input abstract val variant: Property @@ -87,6 +93,9 @@ abstract class BaseAboutLibrariesTask : DefaultTask() { @Input val duplicationRule = extension.library.duplicationRule + @Input + val mergePlatformArtifacts = extension.library.mergePlatformArtifacts + @Input val mapLicensesToSpdx = extension.license.mapLicensesToSpdx @@ -150,6 +159,20 @@ abstract class BaseAboutLibrariesTask : DefaultTask() { @get:Input internal abstract val configToCoordinates: MapProperty> + /** + * Maps configuration name → the Kotlin target name that consumes it (`jvm`, `android`, + * `iosX64`, …), for the configurations selected during [configure]. + * + * Derived from the Kotlin target model ([KotlinTarget.getCompilations]) rather than from the + * `org.jetbrains.kotlin.platform.type` attribute: that attribute is absent on source-set level + * configurations (`jvmMainCompileClasspath`) and cannot tell `wasmJs` from `wasmWasi`. + * + * Empty for projects without the Kotlin plugin; [LibraryPostProcessor] applies the + * configuration-name fallback for anything missing here. + */ + @get:Input + internal abstract val configToTarget: MapProperty + /** * Maps "group:artifact:version" → absolute path of the resolved POM file for every external * module dependency (including transitively discovered parent POMs). @@ -252,13 +275,24 @@ abstract class BaseAboutLibrariesTask : DefaultTask() { configurationNames.set(selectedConfigs.map { it.name }) + // Deferred: the Kotlin plugin may still be applied after this task is configured. Gradle + // finalizes the provider at configuration-cache store time, by which point it is present. + // Captures the extension container rather than the `Project` itself, matching the + // handling of `dependencyHandler`/`configContainer` below. + val selectedConfigNames = selectedConfigs.mapTo(HashSet()) { it.name } + val extensionContainer = project.extensions + configToTarget.set(project.provider { + collectKotlinTargets(extensionContainer).filterKeys { it in selectedConfigNames } + }) + val capturedIncludePlatform = includePlatform.get() + val capturedMergePlatformArtifacts = mergePlatformArtifacts.get() val capturedConfigs = selectedConfigs val dependencyHandler = project.dependencies val configContainer = project.configurations val resolvedProvider = project.provider { - val collector = DependencyCollector(capturedIncludePlatform) + val collector = DependencyCollector(capturedIncludePlatform, capturedMergePlatformArtifacts) val perConfigCoords = LinkedHashMap>(capturedConfigs.size) val unionCoords = LinkedHashSet() for (config in capturedConfigs) { @@ -464,7 +498,9 @@ abstract class BaseAboutLibrariesTask : DefaultTask() { duplicationRule = duplicationRule.get(), variant = variant.orNull, mapLicensesToSpdx = mapLicensesToSpdx.get(), - gitHubToken = gitHubApiToken.orNull + gitHubToken = gitHubApiToken.orNull, + includeTargets = includeTargets.get(), + configToTarget = configToTarget.get(), ) } @@ -505,6 +541,40 @@ abstract class BaseAboutLibrariesTask : DefaultTask() { } } +/** + * Builds a `configurationName` → Kotlin target name map from the project's Kotlin target model. + * + * A configuration is attributed to the target whose compilation declares it as its compile or + * runtime dependency configuration, which is exact (it separates `wasmJs` from `wasmWasi`, and + * `iosX64` from `iosArm64`) where the `org.jetbrains.kotlin.platform.type` attribute is not. + * + * Returns an empty map when the Kotlin plugin is not applied. The `kotlin` extension is looked up + * by name so no Kotlin Gradle Plugin class is touched in that case — the plugin declares KGP as + * `compileOnly`. + * + * Targets with a blank name are skipped: the single-target extensions (`kotlin("android")`, + * `kotlin("jvm")`) name their only target `""`, because there is no sibling to disambiguate it + * from. Such a project compiles for exactly one target, so there is nothing to attribute and + * nothing a consumer could filter by. + */ +private fun collectKotlinTargets(extensions: ExtensionContainer): Map { + val kotlin = extensions.findByName("kotlin") ?: return emptyMap() + val targets = when (kotlin) { + is KotlinTargetsContainer -> kotlin.targets + is KotlinSingleTargetExtension<*> -> listOf(kotlin.target) + else -> return emptyMap() + } + return buildMap { + targets.forEach { target -> + val name = target.targetName.takeIf { it.isNotBlank() } ?: return@forEach + target.compilations.forEach { compilation -> + put(compilation.compileDependencyConfigurationName, name) + compilation.runtimeDependencyConfigurationName?.let { put(it, name) } + } + } + } +} + /** * Serialise a [Pattern] into a configuration-cache-safe regex string that, when recompiled via * `String.toRegex()` (or `Pattern.compile(String)`), matches identically to the original. diff --git a/plugin-build/plugin/src/main/kotlin/com/mikepenz/aboutlibraries/plugin/mapping/Library.kt b/plugin-build/plugin/src/main/kotlin/com/mikepenz/aboutlibraries/plugin/mapping/Library.kt index a1bb1c5e8..9834ef634 100644 --- a/plugin-build/plugin/src/main/kotlin/com/mikepenz/aboutlibraries/plugin/mapping/Library.kt +++ b/plugin-build/plugin/src/main/kotlin/com/mikepenz/aboutlibraries/plugin/mapping/Library.kt @@ -17,6 +17,13 @@ data class Library( var licenses: Set = emptySet(), var funding: Set = emptySet(), var tag: String? = null, + /** + * Kotlin target names this library is consumed by (e.g. `android`, `jvm`, `iosX64`). + * + * `null` unless `collect.includeTargets` is enabled — the JSON writer excludes nulls, so the + * field is omitted from the output entirely when the feature is off. + */ + var targets: Set? = null, var artifactFolder: File? = null, ) { val artifactId: String diff --git a/plugin-build/plugin/src/main/kotlin/com/mikepenz/aboutlibraries/plugin/util/DependencyCollector.kt b/plugin-build/plugin/src/main/kotlin/com/mikepenz/aboutlibraries/plugin/util/DependencyCollector.kt index 01cfb23d7..b69818bc7 100644 --- a/plugin-build/plugin/src/main/kotlin/com/mikepenz/aboutlibraries/plugin/util/DependencyCollector.kt +++ b/plugin-build/plugin/src/main/kotlin/com/mikepenz/aboutlibraries/plugin/util/DependencyCollector.kt @@ -31,6 +31,7 @@ import java.io.InputStream internal class DependencyCollector( private val includePlatform: Boolean = false, + private val mergePlatformArtifacts: Boolean = false, ) { /** @@ -42,7 +43,18 @@ internal class DependencyCollector( */ internal fun loadDependencyCoordinates(root: ResolvedComponentResult): Set { val coordinates = mutableSetOf() - loadDependencyCoordinates(root, coordinates, mutableSetOf()) + // "group:platformArtifact" → module name of the redirect shell it is published under. + val redirects = mutableMapOf() + loadDependencyCoordinates(root, coordinates, redirects, mutableSetOf()) + if (redirects.isEmpty()) return coordinates + // Applied as a pass over the finished set rather than during the walk: a platform artifact + // can be reached directly (e.g. a dependency declaring `annotation-jvm`) before the shell + // that redirects to it is visited, and the graph walk order is not specified. + // Stamped in place — `rootModule` is not part of the coordinate's identity, so mutating it + // cannot disturb the set. + coordinates.forEach { coords -> + redirects["${coords.group}:${coords.artifact}"]?.let { coords.rootModule = it } + } return coordinates } @@ -71,12 +83,24 @@ internal class DependencyCollector( private fun loadDependencyCoordinates( root: ResolvedComponentResult, destination: MutableSet, + redirects: MutableMap, seen: MutableSet, depth: Int = 1, ) { val id = root.id + // Non-null when this component is a pure Gradle `available-at` redirect (a KMP root module + // such as `androidx.collection:collection` pointing at `androidx.collection:collection-jvm`). + // With `mergePlatformArtifacts` the shell itself is dropped and its module name is + // recorded, so the artifact it points at can be reported under the declared coordinate. + val redirectTarget = if (mergePlatformArtifacts) root.redirectTargetModule() else null var ignoreSuffix: String? = null when { + redirectTarget != null -> { + id as ModuleComponentIdentifier + redirects["${id.group}:$redirectTarget"] = id.module + ignoreSuffix = " merge platform artifact $redirectTarget into ${id.module}" + } + id is ProjectComponentIdentifier -> { ignoreSuffix = " skip project dependency" // Local dependency, do nothing. } @@ -122,6 +146,7 @@ internal class DependencyCollector( loadDependencyCoordinates( selected, destination, + redirects, seen, depth + 1, ) @@ -209,7 +234,10 @@ internal class DependencyCollector( LOGGER.debug("--> ArtifactPom for [{}:{}]:\n{}\n\n", pom.groupId, pom.artifactId, pom.pomFile.readText().trim()) } - val uniqueId = pom.groupId + ":" + pom.artifactId + // With `mergePlatformArtifacts` a KMP platform artifact is reported under the root coordinate it was + // resolved through, so the id matches what was declared in the build script. Everything else + // (name, description, licenses, …) still comes from the resolved variant's POM. + val uniqueId = pom.groupId + ":" + (coordinates.rootModule ?: pom.artifactId) // check if we shall skip this specific uniqueId if (shouldSkip(uniqueId)) return null @@ -254,6 +282,23 @@ internal class DependencyCollector( private fun ModuleComponentIdentifier.toDependencyCoordinates() = DependencyCoordinates(group, module, version) + /** + * Returns the module name this component redirects to via Gradle `available-at` + * (`androidx.collection:collection` → `collection-jvm`), or `null` if it is a regular module. + * + * A redirecting component carries no artifacts of its own — every one of its variants only + * points at a variant of the target module. Requiring *all* variants to redirect, and requiring + * a single target within the same group, keeps this from firing on regular modules. + */ + private fun ResolvedComponentResult.redirectTargetModule(): String? { + val self = id as? ModuleComponentIdentifier ?: return null + val owners = runCatching { + variants.map { (it.externalVariant.orElse(null) ?: return null).owner } + }.getOrNull()?.takeIf { it.isNotEmpty() } ?: return null + val target = owners.distinct().singleOrNull() as? ModuleComponentIdentifier ?: return null + return target.module.takeIf { target.group == self.group && it != self.module } + } + private fun ResolvedComponentResult.isPlatform(): Boolean { val singleVariant = variants.singleOrNull() ?: return false val stringAttribute = Attribute.of(CATEGORY_ATTRIBUTE.name, String::class.java) // https://github.com/gradle/gradle/issues/8854 @@ -304,6 +349,22 @@ internal data class DependencyCoordinates( val artifact: String, val version: String, ) : java.io.Serializable { + /** + * Module name of the parent (root) module this artifact was resolved *through*, for Gradle + * `available-at` redirects as used by Kotlin Multiplatform publications + * (`androidx.collection:collection` → `androidx.collection:collection-jvm`). + * + * Only populated when `mergePlatformArtifacts` is enabled; `null` otherwise, and always `null` for + * artifacts that were not reached via a redirect. + * + * Deliberately outside the constructor, so it takes no part in `equals`/`hashCode`: this + * records *how* an artifact was reached, not which artifact it is. The same `g:a:v` can be + * reached through a redirect in one configuration and directly in another; were that two + * distinct coordinates, its POM would be resolved twice and the two would occupy separate + * version slots. `cacheKey()` has always ignored it for the same reason. + */ + var rootModule: String? = null + fun pomCoordinate() = "$group:$artifact:$version@pom" fun cacheKey() = "$group:$artifact:$version" } diff --git a/plugin-build/plugin/src/main/kotlin/com/mikepenz/aboutlibraries/plugin/util/LibraryPostProcessor.kt b/plugin-build/plugin/src/main/kotlin/com/mikepenz/aboutlibraries/plugin/util/LibraryPostProcessor.kt index af79f0eb9..02b42045b 100644 --- a/plugin-build/plugin/src/main/kotlin/com/mikepenz/aboutlibraries/plugin/util/LibraryPostProcessor.kt +++ b/plugin-build/plugin/src/main/kotlin/com/mikepenz/aboutlibraries/plugin/util/LibraryPostProcessor.kt @@ -16,6 +16,7 @@ import org.slf4j.Logger import org.slf4j.LoggerFactory import java.io.File import java.util.Locale +import java.util.TreeSet internal class LibraryPostProcessor( private val variantToDependencyData: Map>, @@ -31,6 +32,8 @@ internal class LibraryPostProcessor( private var variant: String? = null, private val mapLicensesToSpdx: Boolean = true, gitHubToken: String? = null, + private val includeTargets: Boolean = false, + private val configToTarget: Map = emptyMap(), ) { private val additionalLicenses: MutableSet = additionalLicenses.toMutableSet() @@ -59,16 +62,53 @@ internal class LibraryPostProcessor( } else emptySet() val variant = variant - val dependencyDataForVariant = if (variant.isNullOrBlank()) { - variantToDependencyData.flatMap { (_, dependencies) -> dependencies }.deduplicateDependencies() ?: emptySet() + + // The configurations contributing to this task's output. Retained separately from the + // flattened + deduplicated dependency list below, because deduplication collapses entries + // by `uniqueId` and would otherwise discard which configuration each dependency came from. + // + // Ordered runtime classpaths first: when compile and runtime resolve different versions of the + // same module, `deduplicateDependencies` keeps the first entry, and the runtime resolution is + // the version that actually ships. + val selectedConfigs: Map> = when { + variant.isNullOrBlank() -> variantToDependencyData + variantToDependencyData.containsKey(variant) -> mapOf(variant to variantToDependencyData.getValue(variant)) + // if we don't have an exact match, use all variants starting with + else -> variantToDependencyData.filterKeys { configName -> + configName.removeSuffix("CompileClasspath").removeSuffix("RuntimeClasspath") == variant + } + }.toSortedMap(compareBy({ !it.endsWith("RuntimeClasspath") }, { it })) + + val dependencyDataForVariant: Collection? = if (variant.isNullOrBlank()) { + selectedConfigs.flatMap { (_, dependencies) -> dependencies }.deduplicateDependencies() ?: emptySet() } else { - variantToDependencyData[variant] ?: variantToDependencyData.flatMap { (configName, dependencies) -> - // if we don't have an exact match, use all variants starting with - val cleanedConfigName = configName.removeSuffix("CompileClasspath").removeSuffix("RuntimeClasspath") - if (cleanedConfigName == variant) dependencies else emptyList() - }.deduplicateDependencies() + // deduplicate as well on an exact variant hit: with `mergePlatformArtifacts` several + // resolved artifacts can collapse onto the same root uniqueId within one configuration. + // `deduplicateDependencies()` returns null for an empty result, so an exact hit with no + // dependencies must not fall through to the prefix-match branch below. + variantToDependencyData[variant]?.let { it.deduplicateDependencies() ?: emptySet() } + ?: selectedConfigs.flatMap { (_, dependencies) -> dependencies }.deduplicateDependencies() } + // uniqueId -> Kotlin target names consuming it. Sorted so the generated output is + // byte-for-byte stable regardless of configuration iteration order. + val targetsByUniqueId: Map> = if (includeTargets) { + buildMap> { + // longest first, so `iosX64` wins over a hypothetical `ios` when both are declared + val knownTargets = configToTarget.values.distinct().sortedByDescending { it.length } + selectedConfigs.forEach { (configName, dependencies) -> + val target = configToTarget[configName] ?: configName.toFallbackTargetName(knownTargets) + if (target == null) { + LOGGER.info("No Kotlin target owns configuration $configName, omitting it from `targets`") + return@forEach + } + dependencies.forEach { dependency -> + getOrPut(dependency.uniqueId) { sortedSetOf() }.add(target) + } + } + } + } else emptyMap() + if (dependencyDataForVariant != null) { dependencyDataForVariant.onEach { dependencyData -> if (compiledPatterns.isEmpty() || !compiledPatterns.any { pattern -> pattern.matches(dependencyData.uniqueId) }) { @@ -117,6 +157,7 @@ internal class LibraryPostProcessor( licenses.map { it.hash }.toSet(), funding, null, + if (includeTargets) targetsByUniqueId[dependencyData.uniqueId] ?: emptySet() else null, dependencyData.artifactFolder, ) @@ -143,6 +184,9 @@ internal class LibraryPostProcessor( val librariesMap = librariesList.associateBy { it.uniqueId }.toMutableMap() LibraryReader.readLibraries(configFolder).takeIf { it.isNotEmpty() }?.also { customLibs -> customLibs.forEach { lib -> + // never let an override materialize `targets` while the feature is disabled + if (!includeTargets) lib.targets = null + /** Make sure we fetch any additional needed licenses */ fun Library.handleLicenses() { this.licenses.forEach { @@ -168,6 +212,8 @@ internal class LibraryPostProcessor( if (librariesMap.containsKey(lib.uniqueId)) { librariesMap[lib.uniqueId]?.mergeWithCustom() } else { + // config-only library, not part of any resolved configuration + if (includeTargets && lib.targets == null) lib.targets = emptySet() lib.handleLicenses() librariesList.add(lib) librariesMap[lib.uniqueId] = lib @@ -242,6 +288,29 @@ internal class LibraryPostProcessor( return value?.takeIf { it != "null" }?.trimIndent() ?: "" } + /** + * Target name for a configuration no Kotlin *compilation* claims directly. + * + * Covers the source-set level configurations a multiplatform project adds alongside the + * compilation ones (`jvmMainCompileClasspath` next to `jvmCompileClasspath`), by folding them + * into the [knownTargets] entry they are named after. + * + * `null` for anything else. A configuration that matches no declared target is not evidence of + * an undeclared one — an Android-only or `java-library` project builds a single implicit + * target, and naming it after its build variant (`debug`, `release`) would emit a value no + * consumer could ever match against. That is what `export.variant` is for. + */ + private fun String.toFallbackTargetName(knownTargets: List): String? { + // matched case-insensitively: an unprefixed config is `compileClasspath`, a prefixed one + // `jvmMainCompileClasspath` — the same casing rule the configuration selection applies + val stripped = when { + endsWith("CompileClasspath", true) -> dropLast("CompileClasspath".length) + endsWith("RuntimeClasspath", true) -> dropLast("RuntimeClasspath".length) + else -> this + } + return knownTargets.firstOrNull { stripped.startsWith(it) } + } + private fun List.deduplicateDependencies() = groupBy { it.uniqueId }.map { (uniqueId, value) -> diff --git a/plugin-build/plugin/src/main/kotlin/com/mikepenz/aboutlibraries/plugin/util/LibraryUtil.kt b/plugin-build/plugin/src/main/kotlin/com/mikepenz/aboutlibraries/plugin/util/LibraryUtil.kt index fdbd6c49e..5de0084b8 100644 --- a/plugin-build/plugin/src/main/kotlin/com/mikepenz/aboutlibraries/plugin/util/LibraryUtil.kt +++ b/plugin-build/plugin/src/main/kotlin/com/mikepenz/aboutlibraries/plugin/util/LibraryUtil.kt @@ -24,14 +24,18 @@ fun List.processDuplicates( DuplicateMode.MERGE -> { val deDuplicatedList = mutableListOf() mappedLibs().forEach { (_, group) -> - deDuplicatedList.add( - if (group.size > 1) { - // on duplicates, assumption is the shorter title is the base dependency - group.minByOrNull { it.name?.length ?: it.description?.length ?: Int.MAX_VALUE } ?: group.first() - } else { - group.first() - } - ) + val kept = if (group.size > 1) { + // on duplicates, assumption is the shorter title is the base dependency + group.minByOrNull { it.name?.length ?: it.description?.length ?: Int.MAX_VALUE } ?: group.first() + } else { + group.first() + } + // the discarded siblings may have been consumed by targets the survivor was not + // part of; retain the union so no target is silently lost + if (group.size > 1 && group.any { it.targets != null }) { + kept.targets = group.flatMapTo(sortedSetOf()) { it.targets.orEmpty() } + } + deDuplicatedList.add(kept) } return deDuplicatedList } @@ -106,4 +110,12 @@ fun Library.merge(with: Library) { it.addAll(with.funding) it.addAll(orgLib.funding) } -} \ No newline at end of file + + // merge targets, keeping `null` (== feature disabled) if neither side provides any + if (orgLib.targets != null || with.targets != null) { + orgLib.targets = sortedSetOf().also { + it.addAll(with.targets.orEmpty()) + it.addAll(orgLib.targets.orEmpty()) + } + } +} diff --git a/plugin-build/plugin/src/main/kotlin/com/mikepenz/aboutlibraries/plugin/util/parser/LibraryReader.kt b/plugin-build/plugin/src/main/kotlin/com/mikepenz/aboutlibraries/plugin/util/parser/LibraryReader.kt index e25d76a4d..a419c6a96 100644 --- a/plugin-build/plugin/src/main/kotlin/com/mikepenz/aboutlibraries/plugin/util/parser/LibraryReader.kt +++ b/plugin-build/plugin/src/main/kotlin/com/mikepenz/aboutlibraries/plugin/util/parser/LibraryReader.kt @@ -49,6 +49,9 @@ object LibraryReader { funding.add(Funding(it["platform"] ?: "", it["url"] ?: "")) } + // left `null` when absent so overrides never materialize the field in the output + val targets = (c["targets"] as? List<*>)?.mapNotNullTo(sortedSetOf()) { it as? String } + Library( c["uniqueId"] as String, c["artifactVersion"] as? String, @@ -60,7 +63,8 @@ object LibraryReader { scm, licenses, funding, - c["tag"] as? String + c["tag"] as? String, + targets ) } catch (t: Throwable) { LOGGER.error("Could not read the license ($name)", t) diff --git a/plugin-build/plugin/src/test/kotlin/com/mikepenz/aboutlibraries/plugin/Agp10CompatibilityTest.kt b/plugin-build/plugin/src/test/kotlin/com/mikepenz/aboutlibraries/plugin/Agp10CompatibilityTest.kt index 1590e846f..24e111c4b 100644 --- a/plugin-build/plugin/src/test/kotlin/com/mikepenz/aboutlibraries/plugin/Agp10CompatibilityTest.kt +++ b/plugin-build/plugin/src/test/kotlin/com/mikepenz/aboutlibraries/plugin/Agp10CompatibilityTest.kt @@ -3,6 +3,7 @@ package com.mikepenz.aboutlibraries.plugin import org.gradle.testkit.runner.GradleRunner import org.gradle.testkit.runner.TaskOutcome import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertFalse import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.Test import org.junit.jupiter.api.io.TempDir @@ -66,7 +67,40 @@ class Agp10CompatibilityTest { cp.split(File.pathSeparator).joinToString(", ") { "files(\"${it.replace("\\", "\\\\")}\")" } } - private fun setupAndroidProject(projectDir: File, androidPluginId: String, extensionType: String) { + /** + * An Android-only project has a single implicit target, and the single-target Kotlin extension + * names it `""` — reporting that verbatim yields `"targets":[""]`, and falling back to the + * configuration name yields build variants (`debug`, `release`), which are not targets. + * Neither is something a consumer could filter by, so the array stays empty. + */ + @Test + fun `targets field is empty for an android-only module`() { + setupAndroidProject( + projectDir, + "com.android.application", + "com.android.build.api.dsl.ApplicationExtension", + includeTargets = true, + ) + + val result = GradleRunner.create() + .withProjectDir(projectDir) + .withArguments("exportLibraryDefinitionsRelease", "--stacktrace") + .build() + + assertEquals(TaskOutcome.SUCCESS, result.task(":exportLibraryDefinitionsRelease")?.outcome) + + val content = File(projectDir, "build/generated/aboutLibraries/aboutlibraries.json").readText() + assertTrue(content.contains("\"targets\":[]"), "targets must be empty, was: $content") + assertFalse(content.contains("\"targets\":[\"\"]"), "the unnamed single target must not be reported: $content") + assertFalse(content.contains("\"release\""), "build variants are not targets: $content") + } + + private fun setupAndroidProject( + projectDir: File, + androidPluginId: String, + extensionType: String, + includeTargets: Boolean = false, + ) { File(projectDir, "gradle.properties").writeText( """ android.useAndroidX=true @@ -129,6 +163,9 @@ class Agp10CompatibilityTest { extensions.configure("aboutLibraries") { offlineMode = true + collect { + includeTargets = $includeTargets + } } """.trimIndent() ) diff --git a/plugin-build/plugin/src/test/kotlin/com/mikepenz/aboutlibraries/plugin/KmpAndroidFunctionalTest.kt b/plugin-build/plugin/src/test/kotlin/com/mikepenz/aboutlibraries/plugin/KmpAndroidFunctionalTest.kt index 8aaab5356..034a6e066 100644 --- a/plugin-build/plugin/src/test/kotlin/com/mikepenz/aboutlibraries/plugin/KmpAndroidFunctionalTest.kt +++ b/plugin-build/plugin/src/test/kotlin/com/mikepenz/aboutlibraries/plugin/KmpAndroidFunctionalTest.kt @@ -3,6 +3,7 @@ package com.mikepenz.aboutlibraries.plugin import org.gradle.testkit.runner.GradleRunner import org.gradle.testkit.runner.TaskOutcome import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertFalse import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.Test import org.junit.jupiter.api.io.TempDir @@ -90,6 +91,69 @@ class KmpAndroidFunctionalTest { ) } + /** + * `collect.includeTargets` must report the Kotlin *target* names, not the Gradle configuration + * names the dependencies were resolved from. `androidx.annotation` is declared in `androidMain` + * only, so it is the case that distinguishes real per-target attribution from a stub that + * reports every target for every library. + */ + @Test + fun `targets field reports the kotlin targets consuming each dependency`() { + setupKmpAndroidProject(projectDir, includeTargets = true) + + val result = GradleRunner.create() + .withProjectDir(projectDir) + .withArguments("exportLibraryDefinitions", "--stacktrace") + .build() + + assertEquals(TaskOutcome.SUCCESS, result.task(":exportLibraryDefinitions")?.outcome) + + val content = File(projectDir, "build/generated/aboutLibraries/aboutlibraries.json").readText() + val gson = extractLibraryEntry(content, "com.google.code.gson:gson") + ?: error("gson entry not found in output: $content") + // resolves through the KMP `available-at` redirect, so it lands under its platform artifact + val annotation = extractLibraryEntry(content, "androidx.annotation:annotation-jvm") + ?: error("androidx.annotation entry not found in output: $content") + + // no raw configuration name may leak into the field + assertFalse( + content.contains("Classpath\""), + "targets must hold kotlin target names, not configuration names. Output: $content" + ) + + val gsonTargets = targetsOf(gson) + assertTrue( + gsonTargets.containsAll(setOf("android", "jvm")), + "a commonMain dependency is consumed by every target, was: $gsonTargets" + ) + assertEquals( + setOf("android"), + targetsOf(annotation), + "an androidMain-only dependency must report the android target alone. Entry: $annotation" + ) + } + + private fun targetsOf(entry: String): Set = + Regex("\"targets\":\\[(.*?)]").find(entry)?.groupValues?.get(1) + ?.split(",")?.mapNotNull { it.trim().trim('"').takeIf(String::isNotEmpty) }?.toSet() + ?: error("no `targets` field in entry: $entry") + + /** Slices out a single library object by `uniqueId` from the (non pretty-printed) output. */ + private fun extractLibraryEntry(json: String, uniqueId: String): String? { + val keyIdx = json.indexOf("\"uniqueId\":\"$uniqueId\"") + if (keyIdx < 0) return null + var start = keyIdx + while (start > 0 && json[start] != '{') start-- + var depth = 0 + for (end in start until json.length) { + when (json[end]) { + '{' -> depth++ + '}' -> if (--depth == 0) return json.substring(start, end + 1) + } + } + return null + } + private val pluginClasspath: String by lazy { val resource = javaClass.classLoader.getResource("plugin-under-test-metadata.properties") ?: error("plugin-under-test-metadata.properties not found on test classpath") @@ -99,7 +163,7 @@ class KmpAndroidFunctionalTest { cp.split(File.pathSeparator).joinToString(", ") { "files(\"${it.replace("\\", "\\\\")}\")" } } - private fun setupKmpAndroidProject(projectDir: File) { + private fun setupKmpAndroidProject(projectDir: File, includeTargets: Boolean = false) { File(projectDir, "settings.gradle.kts").writeText( """ pluginManagement { @@ -156,6 +220,9 @@ class KmpAndroidFunctionalTest { extensions.configure("aboutLibraries") { offlineMode = true + collect { + includeTargets = $includeTargets + } } """.trimIndent() ) diff --git a/plugin-build/plugin/src/test/kotlin/com/mikepenz/aboutlibraries/plugin/MergePlatformArtifactsFunctionalTest.kt b/plugin-build/plugin/src/test/kotlin/com/mikepenz/aboutlibraries/plugin/MergePlatformArtifactsFunctionalTest.kt new file mode 100644 index 000000000..e991a8973 --- /dev/null +++ b/plugin-build/plugin/src/test/kotlin/com/mikepenz/aboutlibraries/plugin/MergePlatformArtifactsFunctionalTest.kt @@ -0,0 +1,236 @@ +package com.mikepenz.aboutlibraries.plugin + +import org.gradle.testkit.runner.GradleRunner +import org.gradle.testkit.runner.TaskOutcome +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertFalse +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.io.TempDir +import java.io.File + +/** + * Verifies `aboutLibraries.library.mergePlatformArtifacts`: Kotlin Multiplatform artifacts reached + * via a Gradle `available-at` redirect are reported under the root coordinate that was declared, + * while artifacts genuinely published under a suffixed coordinate stay untouched. + * + * `androidx.collection:collection` is a KMP publication: on a JVM classpath it redirects to + * `androidx.collection:collection-jvm`. + */ +class MergePlatformArtifactsFunctionalTest { + + @TempDir + lateinit var projectDir: File + + @Test + fun `root and platform artifact are both collected when merging is disabled`() { + val json = runExport(mergePlatformArtifacts = false) + + // `duplicationMode = KEEP` (set by runExport) keeps both entries visible; with the default + // MERGE they are collapsed onto whichever of the two the graph walk happened to reach first + assertTrue(json.contains("\"androidx.collection:collection-jvm\""), "Expected platform artifact id. Output:\n$json") + assertTrue(json.contains("\"androidx.collection:collection\""), "Expected redirect shell id. Output:\n$json") + } + + @Test + fun `platform artifacts are merged regardless of the order the graph is walked in`() { + // `annotation-jvm` is declared first, so it is reached directly before the `annotation` + // redirect shell that `collection` pulls in transitively. The merge must not depend on + // which path reaches the platform artifact first. + val json = runExport( + mergePlatformArtifacts = true, + dependencies = listOf("androidx.annotation:annotation-jvm:1.9.1", "androidx.collection:collection:1.5.0"), + ) + + assertTrue(json.contains("\"androidx.annotation:annotation\""), "Expected declared root id. Output:\n$json") + assertFalse(json.contains("\"androidx.annotation:annotation-jvm\""), "Platform artifact id must be replaced. Output:\n$json") + } + + @Test + fun `platform artifacts are reported under the declared root coordinate when merging`() { + val json = runExport(mergePlatformArtifacts = true) + + assertTrue(json.contains("\"androidx.collection:collection\""), "Expected declared root id. Output:\n$json") + assertFalse(json.contains("\"androidx.collection:collection-jvm\""), "Platform artifact id must be replaced") + // metadata still comes from the resolved artifact's POM + assertTrue(json.contains("Standalone efficient collections."), "Description of the resolved artifact should be kept") + } + + @Test + fun `non-multiplatform dependency trees are byte-identical with and without merging`() { + // guards against the detection widening beyond `available-at` redirects: a plain JVM tree + // has no redirect shells at all, so enabling the option must be a no-op + val dependencies = listOf("com.google.code.gson:gson:2.11.0", "org.slf4j:slf4j-api:2.0.16") + + val disabled = runExport(mergePlatformArtifacts = false, dependencies = dependencies) + val enabled = runExport(mergePlatformArtifacts = true, dependencies = dependencies) + + assertEquals(disabled, enabled, "Merging must not alter output for non-KMP dependencies") + } + + @Test + fun `suffixed coordinates without a redirect are untouched when merging`() { + // no KMP root module in the graph, so there is nothing to merge into — the `-jvm` suffix + // must not be stripped by name + val json = runExport(mergePlatformArtifacts = true, dependencies = listOf("androidx.annotation:annotation-jvm:1.9.1")) + + assertTrue(json.contains("\"androidx.annotation:annotation-jvm\""), "Suffixed id without redirect must be kept. Output:\n$json") + assertFalse(json.contains("\"androidx.annotation:annotation\""), "Nothing should have been merged. Output:\n$json") + } + + /** + * The case the option exists for, and the only one a `java-library` project cannot show: a real + * multiplatform *consumer*, where one declared dependency resolves to a different artifact per + * target. + * + * Without merging, the default [DuplicateMode.MERGE] still collapses the three coordinates onto + * one entry — but onto whichever the graph walk reached first. That survivor is named for a + * single platform while standing for all of them, which `collect.includeTargets` makes plainly + * visible: an entry called `collection-js` reporting that it is consumed by `jvm`. + * + * With merging the id is the declared root coordinate and the targets are the union, so the + * name and the targets finally agree. + */ + @Test + fun `merged entry is named for the declared root and carries the union of its targets`() { + val merged = extractEntry(runKmpExport(mergePlatformArtifacts = true), "androidx.collection:collection") + ?: error("expected the declared root coordinate to be reported") + assertEquals(setOf("js", "jvm"), targetsOf(merged) - "metadata", "Entry: $merged") + + val unmerged = runKmpExport(mergePlatformArtifacts = false) + assertFalse( + unmerged.contains("\"uniqueId\":\"androidx.collection:collection\","), + "without merging the survivor is a platform artifact, not the root. Output:\n$unmerged" + ) + } + + private fun targetsOf(entry: String): Set = + Regex("\"targets\":\\[(.*?)]").find(entry)?.groupValues?.get(1) + ?.split(",")?.mapNotNull { it.trim().trim('"').takeIf(String::isNotEmpty) }?.toSet() + ?: error("no `targets` field in entry: $entry") + + /** Slices out a single library object by `uniqueId` from the (non pretty-printed) output. */ + private fun extractEntry(json: String, uniqueId: String): String? { + val keyIdx = json.indexOf("\"uniqueId\":\"$uniqueId\",") + if (keyIdx < 0) return null + var start = keyIdx + while (start > 0 && json[start] != '{') start-- + var depth = 0 + for (end in start until json.length) { + when (json[end]) { + '{' -> depth++ + '}' -> if (--depth == 0) return json.substring(start, end + 1) + } + } + return null + } + + /** + * A Kotlin Multiplatform project with two targets, so `androidx.collection:collection` resolves + * to `collection-jvm` on one and `collection-js` on the other. `duplicationMode` is left at its + * default here — the point is what a normal consumer sees. + */ + private fun runKmpExport(mergePlatformArtifacts: Boolean): String { + File(projectDir, "settings.gradle.kts").writeText( + """ + pluginManagement { repositories { gradlePluginPortal(); mavenCentral(); google() } } + rootProject.name = "kmp-consumer" + """.trimIndent() + ) + File(projectDir, "build.gradle.kts").writeText( + """ + buildscript { + repositories { mavenCentral(); google(); gradlePluginPortal() } + dependencies { + classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion") + classpath(files($pluginClasspath)) + } + } + + apply(plugin = "org.jetbrains.kotlin.multiplatform") + apply(plugin = "com.mikepenz.aboutlibraries.plugin") + + repositories { mavenCentral(); google() } + + extensions.configure { + jvm() + js { nodejs() } + sourceSets.getByName("commonMain").dependencies { + implementation("androidx.collection:collection:1.5.0") + } + } + + extensions.configure("aboutLibraries") { + offlineMode = true + collect { includeTargets = true } + library { mergePlatformArtifacts = $mergePlatformArtifacts } + } + """.trimIndent() + ) + + val result = GradleRunner.create() + .withProjectDir(projectDir) + .withArguments("exportLibraryDefinitions", "--stacktrace") + .build() + + assertEquals(TaskOutcome.SUCCESS, result.task(":exportLibraryDefinitions")?.outcome) + return File(projectDir, "build/generated/aboutLibraries/aboutlibraries.json").readText() + } + + private val kotlinVersion: String = System.getProperty("test.kotlin.version") + ?: error("test.kotlin.version system property must be set by the test task") + + private val pluginClasspath: String by lazy { + val resource = javaClass.classLoader.getResource("plugin-under-test-metadata.properties") + ?: error("plugin-under-test-metadata.properties not found on test classpath") + val props = java.util.Properties().apply { resource.openStream().use { load(it) } } + val cp = props.getProperty("implementation-classpath") + ?: error("implementation-classpath missing from plugin-under-test-metadata.properties") + cp.split(File.pathSeparator).joinToString(", ") { "files(\"${it.replace("\\", "\\\\")}\")" } + } + + private fun runExport( + mergePlatformArtifacts: Boolean, + dependencies: List = listOf("androidx.collection:collection:1.5.0", "androidx.annotation:annotation-jvm:1.9.1"), + ): String { + File(projectDir, "settings.gradle.kts").writeText("""rootProject.name = "test-project"""") + File(projectDir, "build.gradle.kts").writeText( + """ + plugins { + id("java-library") + id("com.mikepenz.aboutlibraries.plugin") + } + + repositories { + mavenCentral() + google() + } + + dependencies { + ${dependencies.joinToString("\n ") { "implementation(\"$it\")" }} + } + + aboutLibraries { + offlineMode = true + library { + mergePlatformArtifacts = $mergePlatformArtifacts + duplicationMode = com.mikepenz.aboutlibraries.plugin.DuplicateMode.KEEP + } + } + """.trimIndent() + ) + + @Suppress("WithPluginClasspathUsage") + val result = GradleRunner.create() + .withProjectDir(projectDir) + .withPluginClasspath() + .withArguments("exportLibraryDefinitions", "--stacktrace") + .build() + + assertEquals(TaskOutcome.SUCCESS, result.task(":exportLibraryDefinitions")?.outcome) + + val outputFile = File(projectDir, "build/generated/aboutLibraries/aboutlibraries.json") + assertTrue(outputFile.exists(), "Output file should be created") + return outputFile.readText() + } +} diff --git a/plugin-build/plugin/src/test/kotlin/com/mikepenz/aboutlibraries/plugin/OutputCorrectnessTest.kt b/plugin-build/plugin/src/test/kotlin/com/mikepenz/aboutlibraries/plugin/OutputCorrectnessTest.kt index 6d136207e..736ca050e 100644 --- a/plugin-build/plugin/src/test/kotlin/com/mikepenz/aboutlibraries/plugin/OutputCorrectnessTest.kt +++ b/plugin-build/plugin/src/test/kotlin/com/mikepenz/aboutlibraries/plugin/OutputCorrectnessTest.kt @@ -389,6 +389,61 @@ class OutputCorrectnessTest { assertTrue(content.contains("\"website\""), "At least one library should have website") } + /** + * `collect.includeTargets` is opt-in. With it disabled (the default) the `targets` key must be + * absent from the output entirely — this guards the committed `aboutlibraries.json` fixtures + * against churn. + */ + @Test + fun `targets field is absent by default`() { + setupProject( + projectDir, + deps = """ + implementation("com.google.code.gson:gson:2.11.0") + """.trimIndent() + ) + + run("exportLibraryDefinitions") + val content = readOutput() + + assertTrue(content.contains("\"uniqueId\":\"com.google.code.gson:gson\""), "gson should be present") + assertFalse(content.contains("\"targets\""), "targets must not be emitted unless opted in") + } + + /** + * A single-target project — `java-library` here, but equally `kotlin("jvm")` or an Android-only + * build — has exactly one implicit target, so the key is emitted but empty rather than filled + * with a configuration or build-variant name no consumer could match against. + * + * Note the single-target Kotlin extensions name their only target `""`; reporting that verbatim + * would emit `"targets":[""]`. + * + * The multi-target behaviour this field exists for is covered by [KmpAndroidFunctionalTest]. + */ + @Test + fun `targets field is empty for a project without kotlin targets`() { + setupProject( + projectDir, + deps = """ + implementation("com.google.code.gson:gson:2.11.0") + """.trimIndent(), + extraConfig = """ + collect { + includeTargets = true + } + """.trimIndent() + ) + + run("exportLibraryDefinitions") + val entry = extractLibraryEntry(readOutput(), "com.google.code.gson:gson") + + assertTrue(entry != null, "gson entry should be present") + assertTrue( + entry!!.contains("\"targets\":[]"), + "a non-Kotlin project has no targets to report, was: $entry" + ) + } + /** * `exclusionPatterns` with regex must work for arbitrary patterns including alternation. */