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