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
+}