From ff9f8986bb7f6b3029e81fb1ddfa9944f4a3e65a Mon Sep 17 00:00:00 2001 From: soloturn Date: Sat, 19 Sep 2026 04:02:32 +0200 Subject: [PATCH] fix: stop PrepareTask discarding the Configuration Cache entry PrepareTask held a ProjectEnv - and with it a live Project - as task state. Any task holding a Project reference is incompatible with the Gradle Configuration Cache, and an incompatible task discards the cache entry for the whole build on every run, not just for itself. So any project applying this plugin could never benefit from the Configuration Cache, even though nothing else in the build was incompatible. Fix: PrepareTask now only holds plain values and Provider/Property instances set via project.provider { } for values (AGP version, application id, project dir path) that are only reliable once the project has finished evaluating. GradleBuildTracker and ObjectBoxGradlePlugin.createPrepareTask are updated accordingly, and the now-unused ProjectEnv-based androidAppId() helper is removed. Adds assembleWithConfigurationCache() regression test: runs assembleDebug twice with --configuration-cache and asserts the first run stores a clean cache entry and the second run reuses it. Fixes objectbox/objectbox-java#948 --- .../io/objectbox/gradle/BuildEventInfo.kt | 34 +++++ .../io/objectbox/gradle/GradleBuildTracker.kt | 31 ++-- .../objectbox/gradle/ObjectBoxGradlePlugin.kt | 28 +++- .../kotlin/io/objectbox/gradle/PrepareTask.kt | 58 ++++++-- .../gradle/AndroidProjectPluginTest.kt | 134 +++++++++++------- .../io/objectbox/gradle/BuildTrackerTest.kt | 44 ++---- 6 files changed, 213 insertions(+), 116 deletions(-) create mode 100644 objectbox-gradle-plugin/src/main/kotlin/io/objectbox/gradle/BuildEventInfo.kt diff --git a/objectbox-gradle-plugin/src/main/kotlin/io/objectbox/gradle/BuildEventInfo.kt b/objectbox-gradle-plugin/src/main/kotlin/io/objectbox/gradle/BuildEventInfo.kt new file mode 100644 index 00000000..899bf064 --- /dev/null +++ b/objectbox-gradle-plugin/src/main/kotlin/io/objectbox/gradle/BuildEventInfo.kt @@ -0,0 +1,34 @@ +/* + * ObjectBox Build Tools + * Copyright (C) 2020-2024 ObjectBox Ltd. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package io.objectbox.gradle + +/** + * The plain, already-resolved facts about a project that [GradleBuildTracker] needs to build a + * "Build" analytics event - deliberately holding no reference to [ProjectEnv] or + * [org.gradle.api.Project], which [PrepareTask] (the only task that uses this) cannot keep as + * task state without breaking the Gradle Configuration Cache. + */ +data class BuildEventInfo( + val hasAndroidPlugin: Boolean, + val hasKotlinAndroidPlugin: Boolean, + val hasKotlinPlugin: Boolean, + val hasJavaPlugin: Boolean, + val agpVersion: String? = null, + val applicationId: String? = null +) diff --git a/objectbox-gradle-plugin/src/main/kotlin/io/objectbox/gradle/GradleBuildTracker.kt b/objectbox-gradle-plugin/src/main/kotlin/io/objectbox/gradle/GradleBuildTracker.kt index 75abe503..abdbc432 100644 --- a/objectbox-gradle-plugin/src/main/kotlin/io/objectbox/gradle/GradleBuildTracker.kt +++ b/objectbox-gradle-plugin/src/main/kotlin/io/objectbox/gradle/GradleBuildTracker.kt @@ -18,7 +18,6 @@ package io.objectbox.gradle -import io.objectbox.gradle.util.AndroidCompat import io.objectbox.reporting.BasicBuildTracker import org.gradle.util.GradleVersion import java.util.* @@ -33,19 +32,19 @@ open class GradleBuildTracker(toolName: String) : BasicBuildTracker(toolName) { return ProjectEnv.Const.OBX_PLUGIN_VERSION } - fun trackBuild(env: ProjectEnv) { + fun trackBuild(info: BuildEventInfo) { countBuild() if (shouldSendBuildEvent()) { - sendEvent("Build", buildEventProperties(env)) + sendEvent("Build", buildEventProperties(info)) } } // Use internal once fixed (Kotlin 1.1.4?) - fun buildEventProperties(env: ProjectEnv): String { + fun buildEventProperties(info: BuildEventInfo): String { val event = StringBuilder() // AAID: Anonymous App ID - val appId = androidAppId(env) + val appId = info.applicationId if (appId != null) { event.key("AAID").value(hashBase64WithoutPadding(appId)).comma() } @@ -58,30 +57,18 @@ open class GradleBuildTracker(toolName: String) : BasicBuildTracker(toolName) { event.key("CI").value(ci).comma() } // There may be multiple languages in a project, so it's not a single dimension - val hasKotlinPlugin = env.hasKotlinAndroidPlugin || env.hasKotlinPlugin + val hasKotlinPlugin = info.hasKotlinAndroidPlugin || info.hasKotlinPlugin event.key("Kotlin").value(hasKotlinPlugin.toString()).comma() - event.key("Java").value(env.hasJavaPlugin.toString()).comma() + event.key("Java").value(info.hasJavaPlugin.toString()).comma() event.key("Version").value(ProjectEnv.Const.OBX_PLUGIN_VERSION).comma() - event.key("Target").value(if (env.hasAndroidPlugin) "Android" else "Other").comma() - if (env.hasAndroidPlugin) { - event.key("AGP").value(AndroidCompat.getPluginVersion(env.project)).comma() + event.key("Target").value(if (info.hasAndroidPlugin) "Android" else "Other").comma() + if (info.hasAndroidPlugin && info.agpVersion != null) { + event.key("AGP").value(info.agpVersion).comma() } event.key("Gradle").value(GradleVersion.current().version) return event.toString() } - /** - * Returns the application ID of the first found build variant. - */ - // Open to allow mocking for testing. - open fun androidAppId(env: ProjectEnv): String? { - if (!env.hasAndroidPlugin) { - return null // Android plugin API not available - } - val project = env.project - return AndroidCompat.getPlugin(project).getFirstApplicationId(project) - } - private fun checkCI(): String? { return when { // https://docs.github.com/en/actions/learn-github-actions/variables#default-environment-variables diff --git a/objectbox-gradle-plugin/src/main/kotlin/io/objectbox/gradle/ObjectBoxGradlePlugin.kt b/objectbox-gradle-plugin/src/main/kotlin/io/objectbox/gradle/ObjectBoxGradlePlugin.kt index 77265595..f93bc282 100644 --- a/objectbox-gradle-plugin/src/main/kotlin/io/objectbox/gradle/ObjectBoxGradlePlugin.kt +++ b/objectbox-gradle-plugin/src/main/kotlin/io/objectbox/gradle/ObjectBoxGradlePlugin.kt @@ -25,6 +25,7 @@ import io.objectbox.gradle.transform.ObjectBoxJavaTransform import io.objectbox.gradle.transform.TransformException import io.objectbox.gradle.util.AndroidCompat import io.objectbox.gradle.util.GradleCompat +import io.objectbox.reporting.ObjectBoxBuildConfig import org.gradle.api.Action import org.gradle.api.Plugin import org.gradle.api.Project @@ -133,7 +134,32 @@ open class ObjectBoxGradlePlugin : Plugin { // use register to defer creation until use val prepareTaskName = "objectboxPrepareBuild" - val prepareTask = project.tasks.register(prepareTaskName, PrepareTask::class.java, env, buildTracker) + // Only the plugin-presence flags are safe to resolve now: they are plain + // `plugins.hasPlugin(...)` checks, correct as soon as this plugin applies. Everything + // else PrepareTask needs (project dir, AGP version, application id, output file) is + // configured below via `project.provider { }`/`project.layout`, deferring the actual + // Project access until Gradle resolves those providers - never storing `project`/`env` + // as task state, which is what broke Configuration Cache support before (see + // PrepareTask's docs and https://github.com/objectbox/objectbox-java/issues/948). + val prepareTask = project.tasks.register( + prepareTaskName, + PrepareTask::class.java, + env.hasAndroidPlugin, + env.hasKotlinAndroidPlugin, + env.hasKotlinPlugin, + env.hasJavaPlugin, + buildTracker + ) + prepareTask.configure { task -> + task.projectDirPath.set(project.provider { project.projectDir.absolutePath }) + task.buildConfigFile.set(project.layout.buildDirectory.file(ObjectBoxBuildConfig.FILE_NAME)) + if (env.hasAndroidPlugin) { + task.agpVersion.set(project.provider { AndroidCompat.getPluginVersion(project) }) + task.applicationId.set( + project.provider { AndroidCompat.getPlugin(project).getFirstApplicationId(project) } + ) + } + } env.logDebug { "Registered $prepareTaskName task." } // make build task depend on prepare task diff --git a/objectbox-gradle-plugin/src/main/kotlin/io/objectbox/gradle/PrepareTask.kt b/objectbox-gradle-plugin/src/main/kotlin/io/objectbox/gradle/PrepareTask.kt index 5bb720ed..1aa5b90a 100644 --- a/objectbox-gradle-plugin/src/main/kotlin/io/objectbox/gradle/PrepareTask.kt +++ b/objectbox-gradle-plugin/src/main/kotlin/io/objectbox/gradle/PrepareTask.kt @@ -20,24 +20,48 @@ package io.objectbox.gradle import io.objectbox.reporting.ObjectBoxBuildConfig import org.gradle.api.DefaultTask +import org.gradle.api.file.RegularFileProperty +import org.gradle.api.provider.Property +import org.gradle.api.tasks.Internal import org.gradle.api.tasks.OutputFile import org.gradle.api.tasks.TaskAction -import java.io.File import javax.inject.Inject - /** * Writes build config file required for processor and tracks builds. + * + * Only ever holds plain values (or, where the real value is not known until after the project + * has been evaluated, a [Property] set from a lazy `project.provider { }`) as task state - + * never a [org.gradle.api.Project] or [ProjectEnv] directly. Both are unsupported by the Gradle + * Configuration Cache: an incompatible task discards the cache entry for the whole build on + * every run, not just for itself (see https://github.com/objectbox/objectbox-java/issues/948). + * + * The constructor-injected flags are static per project (plain `plugins.hasPlugin(...)` checks, + * correct as soon as the plugin applies), so they can be passed in directly. [projectDirPath], + * [agpVersion], [applicationId] and [buildConfigFile] are set by the registering code via + * `project.provider { }`/`project.layout` instead, since - like `env.project.buildDir` and + * `AndroidCompat.getFirstApplicationId(project)` in the pre-fix version of this task - their + * real value is only reliable once the project's own build script has finished configuring it. */ -open class PrepareTask @Inject constructor( - private val env: ProjectEnv, +abstract class PrepareTask @Inject constructor( + private val hasAndroidPlugin: Boolean, + private val hasKotlinAndroidPlugin: Boolean, + private val hasKotlinPlugin: Boolean, + private val hasJavaPlugin: Boolean, private val buildTracker: GradleBuildTracker ) : DefaultTask() { - private val buildDir = env.project.buildDir + @get:Internal + abstract val projectDirPath: Property + + @get:Internal + abstract val agpVersion: Property + + @get:Internal + abstract val applicationId: Property - @OutputFile - val buildConfigFile: File = ObjectBoxBuildConfig.buildFile(buildDir) + @get:OutputFile + abstract val buildConfigFile: RegularFileProperty init { group = "objectbox" @@ -45,13 +69,27 @@ open class PrepareTask @Inject constructor( @TaskAction fun run() { - buildTracker.trackBuild(env) + buildTracker.trackBuild( + BuildEventInfo( + hasAndroidPlugin = hasAndroidPlugin, + hasKotlinAndroidPlugin = hasKotlinAndroidPlugin, + hasKotlinPlugin = hasKotlinPlugin, + hasJavaPlugin = hasJavaPlugin, + agpVersion = agpVersion.orNull, + applicationId = applicationId.orNull + ) + ) writeBuildConfig() } private fun writeBuildConfig() { + val file = buildConfigFile.get().asFile + // ObjectBoxBuildConfig.buildFile(outputDir) previously did this as a side effect of + // resolving the output path; do it here instead, now that resolving the path itself is + // lazy and side-effect-free. + file.parentFile?.let { if (!it.exists()) it.mkdirs() } // Note: currently not setting Android flavor. - ObjectBoxBuildConfig(env.project.projectDir.absolutePath, null).writeInto(buildConfigFile) + ObjectBoxBuildConfig(projectDirPath.get(), null).writeInto(file) } -} \ No newline at end of file +} diff --git a/objectbox-gradle-plugin/src/test/kotlin/io/objectbox/gradle/AndroidProjectPluginTest.kt b/objectbox-gradle-plugin/src/test/kotlin/io/objectbox/gradle/AndroidProjectPluginTest.kt index 1b1de2f4..19563ac1 100644 --- a/objectbox-gradle-plugin/src/test/kotlin/io/objectbox/gradle/AndroidProjectPluginTest.kt +++ b/objectbox-gradle-plugin/src/test/kotlin/io/objectbox/gradle/AndroidProjectPluginTest.kt @@ -69,8 +69,56 @@ abstract class AndroidProjectPluginTest { */ abstract val buildTransformDirectory: String - @Test - fun assemble() { + @Language("Java") + private val exampleEntitySource = + """ + package com.example; + + import io.objectbox.annotation.Convert; + import io.objectbox.annotation.Entity; + import io.objectbox.annotation.Id; + import io.objectbox.annotation.Transient; + import io.objectbox.converter.PropertyConverter; + import io.objectbox.relation.ToMany; + import io.objectbox.relation.ToOne; + import java.util.List; + + @Entity + public class ExampleEntity { + @Id public long id; + + public ToOne toOneProperty; + public ToMany toManyProperty; + public List toManyListProperty; + + public transient ToOne transientProperty; + @Transient public ToMany transientProperty2; + @Convert(converter = TestConverter.class, dbType = String.class) + public List convertProperty; + + public ExampleEntity(String log) { + toManyProperty = new ToMany<>(this, ExampleEntity_.toManyProperty); + System.out.println(log); + } + + public ExampleEntity() { + this("calls other constructor"); + } + + public static class TestConverter implements PropertyConverter, String> { + @Override + public List convertToEntityProperty(String databaseValue) { + return null; + } + @Override + public String convertToDatabaseValue(List entityProperty) { + return null; + } + } + } + """.trimIndent() + + private fun newGradleRunner(): GradleTestRunner { val gradleRunner = GradleTestRunner(testProjectDir) .apply { // Note: classpath for plugins configured in build script of this project (see GradleTestRunner.build). @@ -82,56 +130,13 @@ abstract class AndroidProjectPluginTest { writeText(androidManifest) } - @Language("Java") - val exampleEntitySource = - """ - package com.example; - - import io.objectbox.annotation.Convert; - import io.objectbox.annotation.Entity; - import io.objectbox.annotation.Id; - import io.objectbox.annotation.Transient; - import io.objectbox.converter.PropertyConverter; - import io.objectbox.relation.ToMany; - import io.objectbox.relation.ToOne; - import java.util.List; - - @Entity - public class ExampleEntity { - @Id public long id; - - public ToOne toOneProperty; - public ToMany toManyProperty; - public List toManyListProperty; - - public transient ToOne transientProperty; - @Transient public ToMany transientProperty2; - @Convert(converter = TestConverter.class, dbType = String.class) - public List convertProperty; - - public ExampleEntity(String log) { - toManyProperty = new ToMany<>(this, ExampleEntity_.toManyProperty); - System.out.println(log); - } - - public ExampleEntity() { - this("calls other constructor"); - } - - public static class TestConverter implements PropertyConverter, String> { - @Override - public List convertToEntityProperty(String databaseValue) { - return null; - } - @Override - public String convertToDatabaseValue(List entityProperty) { - return null; - } - } - } - """.trimIndent() - gradleRunner.addSourceFile("ExampleEntity.java", exampleEntitySource) + return gradleRunner + } + + @Test + fun assemble() { + val gradleRunner = newGradleRunner() val result = gradleRunner.build(listOf("--stacktrace", "assembleDebug"), additionalRunnerConfiguration) assertThat(result.task(":assembleDebug")!!.outcome).isEqualTo(TaskOutcome.SUCCESS) @@ -189,4 +194,31 @@ abstract class AndroidProjectPluginTest { } + /** + * Regression test for https://github.com/objectbox/objectbox-java/issues/948: applying this + * plugin used to make the whole build's Configuration Cache entry unstorable (not just for + * the plugin's own task), because [PrepareTask] held a [ProjectEnv] - and with it a live + * [org.gradle.api.Project] - as task state. Runs `assembleDebug` twice with the + * Configuration Cache enabled: the first run must store a cache entry with no problems, the + * second run must reuse it instead of reconfiguring from scratch. + */ + @Test + fun assembleWithConfigurationCache() { + val gradleRunner = newGradleRunner() + + val firstRun = gradleRunner.build( + listOf("--configuration-cache", "assembleDebug"), + additionalRunnerConfiguration + ) + assertThat(firstRun.task(":assembleDebug")!!.outcome).isEqualTo(TaskOutcome.SUCCESS) + assertThat(firstRun.output).doesNotContain("problem was found storing the configuration cache") + assertThat(firstRun.output).doesNotContain("Configuration cache entry discarded") + + val secondRun = gradleRunner.build( + listOf("--configuration-cache", "assembleDebug"), + additionalRunnerConfiguration + ) + assertThat(secondRun.output).contains("Reusing configuration cache.") + } + } \ No newline at end of file diff --git a/objectbox-gradle-plugin/src/test/kotlin/io/objectbox/gradle/BuildTrackerTest.kt b/objectbox-gradle-plugin/src/test/kotlin/io/objectbox/gradle/BuildTrackerTest.kt index bb52b4ce..f90a7e1b 100644 --- a/objectbox-gradle-plugin/src/test/kotlin/io/objectbox/gradle/BuildTrackerTest.kt +++ b/objectbox-gradle-plugin/src/test/kotlin/io/objectbox/gradle/BuildTrackerTest.kt @@ -21,52 +21,30 @@ package io.objectbox.gradle import com.squareup.moshi.Moshi import com.squareup.moshi.Types import io.objectbox.reporting.BasicBuildTracker.Event -import org.gradle.api.Project -import org.gradle.api.logging.Logger -import org.gradle.api.plugins.ExtensionContainer -import org.gradle.api.plugins.PluginContainer import org.gradle.util.GradleVersion import org.junit.Assert.assertEquals import org.junit.Assert.assertNotNull import org.junit.Assert.assertTrue import org.junit.Test -import org.mockito.ArgumentMatchers -import org.mockito.Mockito.doReturn -import org.mockito.Mockito.mock import org.mockito.Mockito.spy -import org.mockito.Mockito.`when` -import java.io.File class BuildTrackerTest { private val toolName = "TestTool" @Test fun testBuildEventData() { - val project = mock(Project::class.java) - `when`(project.file(ArgumentMatchers.anyString())).thenReturn(mock(File::class.java)) - val pluginContainer = mock(PluginContainer::class.java) - // Mocking "find" did not work out... Avoiding the call... -// `when`(pluginContainer.find>>(ArgumentMatchers.any())).thenReturn(emptyList>()) -// `when`(pluginContainer.find(any())).thenReturn(emptyList>()) -// doReturn(emptyList>()).`when`(pluginContainer).find(any()) - `when`(project.plugins).thenReturn(pluginContainer) - `when`(project.logger).thenReturn(mock(Logger::class.java)) - - val extensionContainer = mock(ExtensionContainer::class.java) - `when`(project.extensions).thenReturn(extensionContainer) - - val options = mock(ObjectBoxPluginExtension::class.java) - `when`(extensionContainer.create(ProjectEnv.Const.EXTENSION_NAME, ObjectBoxPluginExtension::class.java)).thenReturn( - options - ) - - val env = ProjectEnv(project) - val toolName = "TestTool" val analytics = spy(GradleBuildTracker(toolName)) val aid = "my.test.app" - doReturn(aid).`when`(analytics).androidAppId(env) + val info = BuildEventInfo( + hasAndroidPlugin = true, + hasKotlinAndroidPlugin = false, + hasKotlinPlugin = false, + hasJavaPlugin = true, + agpVersion = "8.1.0", + applicationId = aid + ) - val eventData = analytics.eventData("Build", analytics.buildEventProperties(env), true, "dummy") + val eventData = analytics.eventData("Build", analytics.buildEventProperties(info), true, "dummy") val json = parseJsonAndAssertBasics(eventData, "Build") @Suppress("UNCHECKED_CAST") @@ -76,6 +54,8 @@ class BuildTrackerTest { assertEquals(toolName, properties["Tool"]) assertEquals(ProjectEnv.Const.OBX_PLUGIN_VERSION, properties["Version"]) assertEquals(GradleVersion.current().version, properties["Gradle"]) + assertEquals("8.1.0", properties["AGP"]) + assertEquals("Android", properties["Target"]) val analytics2 = spy(GradleBuildTracker(toolName)) assertEquals(distinctId, analytics2.uniqueIdentifier()) @@ -119,4 +99,4 @@ class BuildTrackerTest { assertEquals(toolName, properties["Tool"]) return json } -} \ No newline at end of file +}