|
| 1 | +import com.google.auto.value.AutoValue |
| 2 | +import japicmp.model.* |
| 3 | +import me.champeau.gradle.japicmp.JapicmpTask |
| 4 | +import me.champeau.gradle.japicmp.report.Violation |
| 5 | +import me.champeau.gradle.japicmp.report.stdrules.* |
| 6 | + |
| 7 | + |
| 8 | +plugins { |
| 9 | + base |
| 10 | + |
| 11 | + id("me.champeau.gradle.japicmp") |
| 12 | +} |
| 13 | + |
| 14 | +/** |
| 15 | + * The latest *released* version of the project. Evaluated lazily so the work is only done if necessary. |
| 16 | + */ |
| 17 | +val latestReleasedVersion: String by lazy { |
| 18 | + // hack to find the current released version of the project |
| 19 | + val temp: Configuration = configurations.create("tempConfig") { |
| 20 | + resolutionStrategy.cacheChangingModulesFor(0, "seconds") |
| 21 | + resolutionStrategy.cacheDynamicVersionsFor(0, "seconds") |
| 22 | + } |
| 23 | + // pick aws-xray, since it's a stable module that's always there. |
| 24 | + dependencies.add(temp.name, "io.opentelemetry.contrib:opentelemetry-aws-xray:latest.release") |
| 25 | + val moduleVersion = configurations["tempConfig"].resolvedConfiguration.firstLevelModuleDependencies.elementAt(0).moduleVersion |
| 26 | + configurations.remove(temp) |
| 27 | + logger.debug("Discovered latest release version: " + moduleVersion) |
| 28 | + moduleVersion |
| 29 | +} |
| 30 | + |
| 31 | +class AllowNewAbstractMethodOnAutovalueClasses : AbstractRecordingSeenMembers() { |
| 32 | + override fun maybeAddViolation(member: JApiCompatibility): Violation? { |
| 33 | + val allowableAutovalueChanges = setOf(JApiCompatibilityChangeType.METHOD_ABSTRACT_ADDED_TO_CLASS, |
| 34 | + JApiCompatibilityChangeType.METHOD_ADDED_TO_PUBLIC_CLASS, JApiCompatibilityChangeType.ANNOTATION_ADDED) |
| 35 | + if (member.compatibilityChanges.filter { !allowableAutovalueChanges.contains(it.type) }.isEmpty() && |
| 36 | + member is JApiMethod && isAutoValueClass(member.getjApiClass())) |
| 37 | + { |
| 38 | + return Violation.accept(member, "Autovalue will automatically add implementation") |
| 39 | + } |
| 40 | + if (member.compatibilityChanges.isEmpty() && |
| 41 | + member is JApiClass && isAutoValueClass(member)) { |
| 42 | + return Violation.accept(member, "Autovalue class modification is allowed") |
| 43 | + } |
| 44 | + return null |
| 45 | + } |
| 46 | + |
| 47 | + fun isAutoValueClass(japiClass: JApiClass): Boolean { |
| 48 | + return japiClass.newClass.get().getAnnotation(AutoValue::class.java) != null || |
| 49 | + japiClass.newClass.get().getAnnotation(AutoValue.Builder::class.java) != null |
| 50 | + } |
| 51 | +} |
| 52 | + |
| 53 | +class SourceIncompatibleRule : AbstractRecordingSeenMembers() { |
| 54 | + override fun maybeAddViolation(member: JApiCompatibility): Violation? { |
| 55 | + if (!member.isSourceCompatible()) { |
| 56 | + return Violation.error(member, "Not source compatible: $member") |
| 57 | + } |
| 58 | + return null |
| 59 | + } |
| 60 | +} |
| 61 | + |
| 62 | +/** |
| 63 | + * Locate the project's artifact of a particular version. |
| 64 | + */ |
| 65 | +fun findArtifact(version: String): File { |
| 66 | + val existingGroup = group |
| 67 | + try { |
| 68 | + // Temporarily change the group name because we want to fetch an artifact with the same |
| 69 | + // Maven coordinates as the project, which Gradle would not allow otherwise. |
| 70 | + group = "virtual_group" |
| 71 | + val depModule = "io.opentelemetry.contrib:${base.archivesName.get()}:$version@jar" |
| 72 | + val depJar = "${base.archivesName.get()}-$version.jar" |
| 73 | + val configuration: Configuration = configurations.detachedConfiguration( |
| 74 | + dependencies.create(depModule), |
| 75 | + ) |
| 76 | + return files(configuration.files).filter { |
| 77 | + it.name.equals(depJar) |
| 78 | + }.singleFile |
| 79 | + } finally { |
| 80 | + group = existingGroup |
| 81 | + } |
| 82 | +} |
| 83 | + |
| 84 | +// generate the api diff report for any module that is stable and publishes a jar. |
| 85 | +if (project.findProperty("otel.stable") == "true" && !project.name.startsWith("bom")) { |
| 86 | + afterEvaluate { |
| 87 | + tasks { |
| 88 | + val jApiCmp by registering(JapicmpTask::class) { |
| 89 | + dependsOn("jar") |
| 90 | + |
| 91 | + // the japicmp "new" version is either the user-specified one, or the locally built jar. |
| 92 | + val apiNewVersion: String? by project |
| 93 | + val newArtifact = apiNewVersion?.let { findArtifact(it) } |
| 94 | + ?: file(getByName<Jar>("jar").archiveFile) |
| 95 | + newClasspath.from(files(newArtifact)) |
| 96 | + |
| 97 | + // only output changes, not everything |
| 98 | + onlyModified.set(true) |
| 99 | + |
| 100 | + // the japicmp "old" version is either the user-specified one, or the latest release. |
| 101 | + val apiBaseVersion: String? by project |
| 102 | + val baselineVersion = apiBaseVersion ?: latestReleasedVersion |
| 103 | + oldClasspath.from( |
| 104 | + try { |
| 105 | + files(findArtifact(baselineVersion)) |
| 106 | + } catch (e: Exception) { |
| 107 | + // if we can't find the baseline artifact, this is probably one that's never been published before, |
| 108 | + // so publish the whole API. We do that by flipping this flag, and comparing the current against nothing. |
| 109 | + onlyModified.set(false) |
| 110 | + files() |
| 111 | + }, |
| 112 | + ) |
| 113 | + |
| 114 | + // Reproduce defaults from https://github.com/melix/japicmp-gradle-plugin/blob/09f52739ef1fccda6b4310cf3f4b19dc97377024/src/main/java/me/champeau/gradle/japicmp/report/ViolationsGenerator.java#L130 |
| 115 | + // with some changes. |
| 116 | + val exclusions = mutableListOf<String>() |
| 117 | + // Generics are not detected correctly |
| 118 | + exclusions.add("CLASS_GENERIC_TEMPLATE_CHANGED") |
| 119 | + // Allow new default methods on interfaces |
| 120 | + exclusions.add("METHOD_NEW_DEFAULT") |
| 121 | + // Allow adding default implementations for default methods |
| 122 | + exclusions.add("METHOD_ABSTRACT_NOW_DEFAULT") |
| 123 | + // Bug prevents recognizing default methods of superinterface. |
| 124 | + // Fixed in https://github.com/siom79/japicmp/pull/343 but not yet available in me.champeau.gradle.japicmp |
| 125 | + exclusions.add("METHOD_ABSTRACT_ADDED_IN_IMPLEMENTED_INTERFACE") |
| 126 | + compatibilityChangeExcludes.set(exclusions) |
| 127 | + richReport { |
| 128 | + addSetupRule(RecordSeenMembersSetup::class.java) |
| 129 | + addRule(JApiChangeStatus.NEW, SourceCompatibleRule::class.java) |
| 130 | + addRule(JApiChangeStatus.MODIFIED, SourceCompatibleRule::class.java) |
| 131 | + addRule(JApiChangeStatus.UNCHANGED, UnchangedMemberRule::class.java) |
| 132 | + // Allow new abstract methods on autovalue |
| 133 | + addRule(AllowNewAbstractMethodOnAutovalueClasses::class.java) |
| 134 | + addRule(BinaryIncompatibleRule::class.java) |
| 135 | + // Disallow source incompatible changes, which are allowed by default for some reason |
| 136 | + addRule(SourceIncompatibleRule::class.java) |
| 137 | + } |
| 138 | + |
| 139 | + // this is needed so that we only consider the current artifact, and not dependencies |
| 140 | + ignoreMissingClasses.set(true) |
| 141 | + packageExcludes.addAll( |
| 142 | + "*.internal", |
| 143 | + "*.internal.*" |
| 144 | + ) |
| 145 | + annotationExcludes.add("@kotlin.Metadata") |
| 146 | + val baseVersionString = if (apiBaseVersion == null) "latest" else baselineVersion |
| 147 | + txtOutputFile.set( |
| 148 | + apiNewVersion?.let { file("$rootDir/docs/apidiffs/${apiNewVersion}_vs_$baselineVersion/${base.archivesName.get()}.txt") } |
| 149 | + ?: file("$rootDir/docs/apidiffs/current_vs_$baseVersionString/${base.archivesName.get()}.txt"), |
| 150 | + ) |
| 151 | + } |
| 152 | + // have the check task depend on the api comparison task, to make it more likely it will get used. |
| 153 | + named("check") { |
| 154 | + dependsOn(jApiCmp) |
| 155 | + } |
| 156 | + } |
| 157 | + } |
| 158 | +} |
0 commit comments