diff --git a/.cursor/rules/e2e_tests.mdc b/.cursor/rules/e2e_tests.mdc index 3603794cd5..2fbb17c196 100644 --- a/.cursor/rules/e2e_tests.mdc +++ b/.cursor/rules/e2e_tests.mdc @@ -76,6 +76,8 @@ it. Gradle runs the two in parallel. module once it's added to its workflow. - Suite code may only use APIs that exist in the oldest version of its line. Compiled against a newer one, a missing API fails at runtime with `NoSuchMethodError` or `NoClassDefFoundError`. +- The oldest version's aggregate also compiles the module's main code against that version, e.g. + `compileJavaBoot3_2_12`, since nothing else checks that the SDK compiles against it. ## Gotchas diff --git a/build-logic/src/main/kotlin/io/sentry/gradle/SpringBootMatrixPlugin.kt b/build-logic/src/main/kotlin/io/sentry/gradle/SpringBootMatrixPlugin.kt index c0eee3e7b2..e10c5f67d1 100644 --- a/build-logic/src/main/kotlin/io/sentry/gradle/SpringBootMatrixPlugin.kt +++ b/build-logic/src/main/kotlin/io/sentry/gradle/SpringBootMatrixPlugin.kt @@ -6,6 +6,7 @@ import org.gradle.api.Plugin import org.gradle.api.Project import org.gradle.api.file.FileCollection import org.gradle.api.tasks.SourceSetContainer +import org.gradle.api.tasks.compile.JavaCompile import org.gradle.api.tasks.testing.Test import org.gradle.language.base.plugins.LifecycleBasePlugin @@ -30,6 +31,10 @@ abstract class SpringBootMatrixExtension @Inject constructor(private val project * `gradle/spring-boot-versions.json`, and `allIntegrationTestsBoot` for all of them. The CI * workflows read the same file and run each version in its own job. A WAR module uses its Boot * module's line, because the Boot BOM decides its Spring version. + * + * The line's first version must be the oldest. The module's main code compiles against it in + * `compileJavaBoot`, part of that version's aggregate, because the main compilation only + * proves the code compiles against the catalog's version. */ fun versions(line: Int) { val file = project.layout.settingsDirectory.file("gradle/spring-boot-versions.json") @@ -48,6 +53,24 @@ abstract class SpringBootMatrixExtension @Inject constructor(private val project group = LifecycleBasePlugin.VERIFICATION_GROUP } } + + val oldest = versions.first() + val main = sourceSet("main") + val compileClasspath = classpath("main", oldest, compile = true) + val compileJava = project.tasks.named("compileJava", JavaCompile::class.java) + val compileOldest = + project.tasks.register("compileJava${suffix(oldest)}", JavaCompile::class.java) { + description = "Compiles the main Java sources against Spring Boot $oldest." + group = LifecycleBasePlugin.VERIFICATION_GROUP + source = main.java + classpath = compileClasspath + destinationDirectory.set( + project.layout.buildDirectory.dir("classes/java/main${suffix(oldest)}") + ) + options.release.set(compileJava.flatMap { it.options.release }) + options.annotationProcessorPath = main.annotationProcessorPath + } + project.tasks.named(aggregateName(oldest)) { dependsOn(compileOldest) } } /** @@ -74,28 +97,36 @@ abstract class SpringBootMatrixExtension @Inject constructor(private val project } /** [sourceSet]'s output and runtime dependencies, with Spring Boot at [version]. */ - fun runtimeClasspath(sourceSet: String, version: String): FileCollection { + fun runtimeClasspath(sourceSet: String, version: String): FileCollection = + sourceSet(sourceSet).output + classpath(sourceSet, version, compile = false) + + /** [sourceSet]'s compile or runtime classpath dependencies, with Spring Boot at [version]. */ + private fun classpath(sourceSet: String, version: String, compile: Boolean): FileCollection { val sources = sourceSet(sourceSet) - val name = "$sourceSet${suffix(version)}RuntimeClasspath" + val base = + if (compile) sources.compileClasspathConfigurationName + else sources.runtimeClasspathConfigurationName + val only = + if (compile) sources.compileOnlyConfigurationName else sources.runtimeOnlyConfigurationName + val name = "$sourceSet${suffix(version)}${if (compile) "Compile" else "Runtime"}Classpath" val configurations = project.configurations - val dependencies = - configurations.findByName(name) - ?: configurations.create(name) { - isCanBeConsumed = false + return configurations.findByName(name) + ?: configurations + .resolvable(name) { + // The base classpath's parents by name, since copying its extendsFrom would depend on + // what has been added to it by the time this runs. extendsFrom( configurations.getByName(sources.implementationConfigurationName), - configurations.getByName(sources.runtimeOnlyConfigurationName), - ) - attributes.addAllLater( - configurations.getByName(sources.runtimeClasspathConfigurationName).attributes + configurations.getByName(only), ) + attributes.addAllLater(configurations.getByName(base).attributes) // What setting the version in the catalog does: every Spring Boot artifact, the BOM // included, is in this group. Libraries pinned to their own version still win conflicts. resolutionStrategy.eachDependency { if (requested.group == "org.springframework.boot") useVersion(version) } } - return sources.output + dependencies + .get() } private fun sourceSet(name: String) =