diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index df88096..b151a05 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,6 +1,6 @@ [versions] -gratatouille-runtime = "0.2.1" -gratatouille-build-time = "0.2.2-SNAPSHOT-69754c55a2efca3d61b42618af2fbf395df6c471" +gratatouille-runtime = "0.2.3" +gratatouille-build-time = "0.2.3" # Version of KGP we are using to build tapmoc kgp-latest = "2.3.0" ksp = "2.3.3" diff --git a/librarian.root.properties b/librarian.root.properties index 620f01b..48fde2a 100644 --- a/librarian.root.properties +++ b/librarian.root.properties @@ -1,5 +1,5 @@ java.compatibility=11 -kotlin.compatibility=2.0.21 +kotlin.compatibility=1.9.0 kdoc.olderVersions= kdoc.projects=:tapmoc-gradle-plugin,:tapmoc-tasks diff --git a/settings.gradle.kts b/settings.gradle.kts index cddba85..cfd9fde 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -18,8 +18,20 @@ pluginManagement { includeGroupByRegex("com\\.gradleup\\..*") } } + exclusiveContent { + forRepository { gradlePluginPortal() } + filter { + includeModule("org.gradle.toolchains.foojay-resolver-convention", "org.gradle.toolchains.foojay-resolver-convention.gradle.plugin") + includeModule("org.gradle.toolchains", "foojay-resolver") + } + } } } +plugins { + // Auto-provisions the JDK toolchains + id("org.gradle.toolchains.foojay-resolver-convention") version "1.0.0" +} + include(":tapmoc-gradle-plugin") include(":tapmoc-tasks") diff --git a/tapmoc-gradle-plugin/build.gradle.kts b/tapmoc-gradle-plugin/build.gradle.kts index 809ff41..3fd54d7 100644 --- a/tapmoc-gradle-plugin/build.gradle.kts +++ b/tapmoc-gradle-plugin/build.gradle.kts @@ -64,10 +64,49 @@ dependencies { gratatouille { addDependencies = false + // for included builds pluginLocalPublication("com.gradleup.tapmoc") + // for publishToMavenLocal + pluginMarker("com.gradleup.tapmoc") +} + +/** + * Gradle 8.0 does not run on recent JDKs (it supports up to Java 19), so its tests live in a separate + * source set that runs on a Java 17 toolchain. + */ +val gradle8TestCompilation = kotlin.target.compilations.create("gradle8Test") { + associateWith(kotlin.target.compilations.getByName("test")) +} + +dependencies { + add(gradle8TestCompilation.implementationConfigurationName, gradleTestKit()) + add(gradle8TestCompilation.implementationConfigurationName, kotlin("test")) +} + +val gradle8Test = tasks.register("gradle8Test") { + group = "verification" + testClassesDirs = gradle8TestCompilation.output.classesDirs + classpath = gradle8TestCompilation.output.allOutputs + + gradle8TestCompilation.runtimeDependencyFiles + + kotlin.target.compilations.getByName("test").output.allOutputs + javaLauncher.set(javaToolchains.launcherFor { + languageVersion.set(JavaLanguageVersion.of(17)) + }) +} + +tasks.named("check") { + dependsOn(gradle8Test) +} + +val cleanTestProjects = tasks.register("cleanTestProjects") { + description = "Deletes the test projects left over by previous (failed) test runs." + delete(layout.buildDirectory.map { buildDir -> + buildDir.asFile.listFiles().orEmpty().filter { it.name.startsWith("testProject-") } + }) } tasks.withType().configureEach { + dependsOn(cleanTestProjects) dependsOn("publishAllPublicationsToLocalRepository") dependsOn(":tapmoc-tasks:publishAllPublicationsToLocalRepository") } diff --git a/tapmoc-gradle-plugin/src/gradle8Test/kotlin/Gradle8Tests.kt b/tapmoc-gradle-plugin/src/gradle8Test/kotlin/Gradle8Tests.kt new file mode 100644 index 0000000..e87bddd --- /dev/null +++ b/tapmoc-gradle-plugin/src/gradle8Test/kotlin/Gradle8Tests.kt @@ -0,0 +1,14 @@ +import kotlin.test.Test +import org.junit.Assert.assertTrue + +class Gradle8Tests { + @Test + fun dependencyMismatchIsDetectedWithGradle8_0() { + withTestProject("gradle-8-0") { + gradleRunner(it, "build").withGradleVersion("8.0").buildAndFail().apply { + println(output.toString()) + assertTrue(output.contains("targets class file version 55.0 (Java 11) which is newer than supported <= 52 (Java 8).")) + } + } + } +} diff --git a/tapmoc-gradle-plugin/src/main/kotlin/tapmoc/TapmocExtension.kt b/tapmoc-gradle-plugin/src/main/kotlin/tapmoc/TapmocExtension.kt index ef3418f..9a7e33a 100644 --- a/tapmoc-gradle-plugin/src/main/kotlin/tapmoc/TapmocExtension.kt +++ b/tapmoc-gradle-plugin/src/main/kotlin/tapmoc/TapmocExtension.kt @@ -142,7 +142,6 @@ interface TapmocExtension { */ fun checkKotlinStdlibs(severity: Severity) - /** * This is equivalent to calling `checkJavaClassFiles(runtimeConfiguration, severity)` and `checkKotlinMetadata(apiConfiguration, severity)`. * @@ -150,9 +149,10 @@ interface TapmocExtension { * - `runtimeConfiguration` is the outgoing variant containing the runtime dependencies (typically, "apiElements"). * - `apiConfiguration` is the outgoing variant containing the API dependencies (typically, "runtimeElements"). * - * The actual name of the configurations is guessed depending on the applied plugins (Jvm/kmp/android/etc...). + * Tapmoc "reacts" to known configurations such as "apiElements", "runtimeElements", "jvmApiElements", etc... but might not be aware of + * all of them. For an example, if you're using custom Android variant names or custom KMP JVM targets. * - * If the guessing didn't work, call [checkJavaClassFiles] and [checkKotlinMetadata] manually. + * In those cases, call [checkJavaClassFiles] and [checkKotlinMetadata] manually. * * Note: this doesn't call `checkKotlinStdlibs(runtimeConfigurationm, severity)` as kotlin-stdlib is usually upgraded at runtime. One notable exception is Gradle plugins. * If you are developing a Gradle plugin, you may want to call `checkKotlinStdlibs(severity)`. diff --git a/tapmoc-gradle-plugin/src/main/kotlin/tapmoc/internal/TapmocExtensionImpl.kt b/tapmoc-gradle-plugin/src/main/kotlin/tapmoc/internal/TapmocExtensionImpl.kt index 7e41175..828ad38 100644 --- a/tapmoc-gradle-plugin/src/main/kotlin/tapmoc/internal/TapmocExtensionImpl.kt +++ b/tapmoc-gradle-plugin/src/main/kotlin/tapmoc/internal/TapmocExtensionImpl.kt @@ -6,6 +6,7 @@ import org.gradle.api.UnknownDomainObjectException import org.gradle.api.artifacts.Configuration import org.gradle.api.artifacts.component.ModuleComponentIdentifier import org.gradle.api.file.FileCollection +import org.gradle.api.internal.DefaultNamedDomainObjectCollection import org.gradle.api.provider.Property import org.gradle.api.tasks.TaskProvider import org.gradle.language.base.plugins.LifecycleBasePlugin @@ -55,20 +56,20 @@ internal abstract class TapmocExtensionImpl(private val project: Project) : Tapm private fun configurationFor(configuration: String): NamedDomainObjectProvider { val name = lowerCameCase("tapmoc", configuration) - var tapmocConfiguration = try { + val existing: NamedDomainObjectProvider? = try { project.configurations.named(name) } catch (_: UnknownDomainObjectException) { null } - if (tapmocConfiguration == null) { - tapmocConfiguration = project.configurations.register(name) { - it.isCanBeConsumed = false - it.isCanBeResolved = true - it.isVisible = false - it.extendsFrom(project.configurations.getByName(configuration)) - } + if (existing != null) { + return existing + } + return project.configurations.register(name) { + it.isCanBeConsumed = false + it.isCanBeResolved = true + it.isVisible = false + it.extendsFrom(project.configurations.getByName(configuration)) } - return tapmocConfiguration } private fun fileCollectionFor(configuration: String): FileCollection { @@ -86,7 +87,7 @@ internal abstract class TapmocExtensionImpl(private val project: Project) : Tapm } override fun checkJavaClassFiles(severity: Severity) { - reactToPlugins( + reactToConfigurations( onApi = {}, onRuntime = { checkJavaClassFiles(it, severity)} ) @@ -104,7 +105,7 @@ internal abstract class TapmocExtensionImpl(private val project: Project) : Tapm } override fun checkKotlinMetadata(severity: Severity) { - reactToPlugins( + reactToConfigurations( onApi = {checkKotlinMetadata(it, severity) }, onRuntime = {} ) @@ -131,7 +132,7 @@ internal abstract class TapmocExtensionImpl(private val project: Project) : Tapm } override fun checkKotlinStdlibs(severity: Severity) { - reactToPlugins( + reactToConfigurations( onApi = { }, onRuntime = { checkKotlinStdlibs(it, severity) } ) @@ -141,58 +142,49 @@ internal abstract class TapmocExtensionImpl(private val project: Project) : Tapm checkDependencies(Severity.ERROR) } - private fun reactToPlugins(onApi: (String) -> Unit, onRuntime: (String) -> Unit) { - var hasJava = false - var hasKotlinJvm = false - var hasKotlinMultiplatform = false - - project.pluginManager.withPlugin("java") { - if (!hasKotlinJvm) { - onApi("apiElements") - onRuntime("runtimeElements") - } - hasJava = true - } - project.pluginManager.withPlugin("org.jetbrains.kotlin.jvm") { - if (!hasJava) { - onApi("apiElements") - onRuntime("runtimeElements") + private fun reactToConfigurations(onApi: (String) -> Unit, onRuntime: (String) -> Unit) { + val visitedConfigurations = mutableSetOf() + + /** + * Uses internal APIs. Before that, I tried: + * - determining the "good" configurations based on their attributes, and it's a mess that creates resolution errors + * - reacting to plugins being applied but in some cases, the configurations are not always registered from `Plugin::apply` + * + * Instead, "react" to known configuration being added. This is all a giant spaghetti plate, but I have no clue how to make + * things better at this point. + * If this doesn't work, there is always the explicit API that takes a configuration name. + * + * See https://github.com/gradle/gradle/issues/25262 + */ + val onConfiguration = { name: String -> + if (visitedConfigurations.add(name)) { + when (name) { + "apiElements" -> onApi(name) + "runtimeElements" -> onRuntime(name) + "jvmApiElements" -> onApi(name) + "jvmRuntimeElements" -> onRuntime(name) + "releaseApiElements" -> onApi(name) + "releaseRuntimeElements" -> onRuntime(name) + "debugApiElements" -> onApi(name) + "debugRuntimeElements" -> onRuntime(name) + } } - hasKotlinJvm = true } - project.pluginManager.withPlugin("org.jetbrains.kotlin.multiplatform") { - onApi("jvmApiElements") - onRuntime("jvmRuntimeElements") - hasKotlinMultiplatform = true - } - project.pluginManager.withPlugin("com.android.library") { - onApi("releaseApiElements") - onRuntime("releaseRuntimeElements") + // Handle the existing configurations from a snapshot: the callbacks create configurations + // and Gradle 8.0 throws a ConcurrentModificationException if that happens while whenElementKnown() replays them. + project.configurations.names.toList().forEach(onConfiguration) - hasKotlinMultiplatform = true - } - - project.afterEvaluate { - if (!hasJava && !hasKotlinJvm && !hasKotlinMultiplatform) { - val task = project.tasks.findByName("tapmocError") - if (task == null) { - val task2 = project.tasks.register("tapmocError") { - it.doFirst { - error("Tapmoc: checkDependencies() didn't find any supported plugin. Please call `checkJavaClassFiles()` and `checkKotlinMetadata()` instead.") - } - } - addToCheckTask(task2) - } - } + @Suppress("UNCHECKED_CAST") + (project.configurations as DefaultNamedDomainObjectCollection).whenElementKnown { + onConfiguration(it.name) } } + @Suppress("DEPRECATION") override fun checkDependencies(severity: Severity) { - reactToPlugins( - onApi = { checkKotlinMetadata(it, severity) }, - onRuntime = { checkJavaClassFiles(it, severity) } - ) + checkJavaClassFiles(severity) + checkKotlinMetadata(severity) } @Deprecated( diff --git a/tapmoc-gradle-plugin/src/test/kotlin/TestUtils.kt b/tapmoc-gradle-plugin/src/test/kotlin/TestUtils.kt new file mode 100644 index 0000000..a921c3d --- /dev/null +++ b/tapmoc-gradle-plugin/src/test/kotlin/TestUtils.kt @@ -0,0 +1,39 @@ +import java.io.File +import java.util.Properties +import java.util.UUID +import org.gradle.testkit.runner.GradleRunner + +fun gradleRunner(dir: File, vararg args: String): GradleRunner { + return GradleRunner.create() + .withProjectDir(dir) + .withDebug(false) + .withArguments(*args) + .forwardOutput() +} + +fun withTestProject(name: String, block: (File) -> Unit) { + val src = File("testProjects/$name") + // Must stay directly under "build/": the test projects reference "../../../build/m2". + // The directory is unique so that tests can run in parallel. + val dst = File("build/testProject-$name-${UUID.randomUUID()}") + + src.copyRecursively(dst) + + dst.walk().onLeave { + if (it.isDirectory && it.name == "build") { + it.deleteRecursively() + } + }.count() // count is just used to collect the sequence + + val currentVersion = Properties().apply { + File("../librarian.root.properties").reader().use { + load(it) + } + } + listOf("build.gradle.kts", "build.gradle").map { dst.resolve(it) }.first { it.exists() }.let { + it.writeText(it.readText().replace("PLACEHOLDER", currentVersion.get("pom.version").toString())) + } + block(dst) + // Keep the directory around if the test fails so it can be inspected + dst.deleteRecursively() +} diff --git a/tapmoc-gradle-plugin/src/test/kotlin/Tests.kt b/tapmoc-gradle-plugin/src/test/kotlin/Tests.kt index 39b0fd6..49938d3 100644 --- a/tapmoc-gradle-plugin/src/test/kotlin/Tests.kt +++ b/tapmoc-gradle-plugin/src/test/kotlin/Tests.kt @@ -1,18 +1,7 @@ -import java.io.File -import java.util.Properties import kotlin.test.Test -import org.gradle.testkit.runner.GradleRunner import org.junit.Assert.assertTrue class Tests { - private fun gradleRunner(dir: File, vararg args: String): GradleRunner { - return GradleRunner.create() - .withProjectDir(dir) - .withDebug(false) - .withArguments(*args) - .forwardOutput() - } - @Test fun wrongJavaBytecodeIsDetected() { withTestProject("java") { @@ -48,28 +37,3 @@ class Tests { } } } - - -private fun withTestProject(name: String, block: (File) -> Unit) { - val src = File("testProjects/$name") - val dst = File("build/testProject") - dst.deleteRecursively() - - src.copyRecursively(dst) - - dst.walk().onLeave { - if (it.isDirectory && it.name == "build") { - it.deleteRecursively() - } - }.count() // count is just used to collect the sequence - - val currentVersion = Properties().apply { - File("../librarian.root.properties").reader().use { - load(it) - } - } - dst.resolve("build.gradle.kts").let { - it.writeText(it.readText().replace("PLACEHOLDER", currentVersion.get("pom.version").toString())) - } - block(dst) -} diff --git a/tapmoc-gradle-plugin/testProjects/gradle-8-0/build.gradle.kts b/tapmoc-gradle-plugin/testProjects/gradle-8-0/build.gradle.kts new file mode 100644 index 0000000..e16aaf3 --- /dev/null +++ b/tapmoc-gradle-plugin/testProjects/gradle-8-0/build.gradle.kts @@ -0,0 +1,16 @@ +import tapmoc.Severity + +plugins { + id("java") + id("com.gradleup.tapmoc").version("PLACEHOLDER") +} + +extensions.getByType(tapmoc.TapmocExtension::class.java).apply { + java(8) + checkDependencies(Severity.ERROR) +} + +dependencies { + // jetty 11 targets Java 11 (class file version 55) + implementation("org.eclipse.jetty:jetty-util:11.0.20") +} diff --git a/tapmoc-gradle-plugin/testProjects/gradle-8-0/settings.gradle.kts b/tapmoc-gradle-plugin/testProjects/gradle-8-0/settings.gradle.kts new file mode 100644 index 0000000..087fd30 --- /dev/null +++ b/tapmoc-gradle-plugin/testProjects/gradle-8-0/settings.gradle.kts @@ -0,0 +1,6 @@ +pluginManagement { + listOf(repositories, dependencyResolutionManagement.repositories).forEach { + it.mavenCentral() + it.maven("../../../build/m2") + } +}