Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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 <https://www.gnu.org/licenses/>.
*/

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
)
Original file line number Diff line number Diff line change
Expand Up @@ -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.*
Expand All @@ -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()
}
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -133,7 +134,32 @@ open class ObjectBoxGradlePlugin : Plugin<Project> {
// 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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,38 +20,76 @@ 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<String>

@get:Internal
abstract val agpVersion: Property<String>

@get:Internal
abstract val applicationId: Property<String>

@OutputFile
val buildConfigFile: File = ObjectBoxBuildConfig.buildFile(buildDir)
@get:OutputFile
abstract val buildConfigFile: RegularFileProperty

init {
group = "objectbox"
}

@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)
}

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<ExampleEntity> toOneProperty;
public ToMany<ExampleEntity> toManyProperty;
public List<ExampleEntity> toManyListProperty;

public transient ToOne<ExampleEntity> transientProperty;
@Transient public ToMany<ExampleEntity> transientProperty2;
@Convert(converter = TestConverter.class, dbType = String.class)
public List<ExampleEntity> 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<List<ExampleEntity>, String> {
@Override
public List<ExampleEntity> convertToEntityProperty(String databaseValue) {
return null;
}
@Override
public String convertToDatabaseValue(List<ExampleEntity> 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).
Expand All @@ -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<ExampleEntity> toOneProperty;
public ToMany<ExampleEntity> toManyProperty;
public List<ExampleEntity> toManyListProperty;

public transient ToOne<ExampleEntity> transientProperty;
@Transient public ToMany<ExampleEntity> transientProperty2;
@Convert(converter = TestConverter.class, dbType = String.class)
public List<ExampleEntity> 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<List<ExampleEntity>, String> {
@Override
public List<ExampleEntity> convertToEntityProperty(String databaseValue) {
return null;
}
@Override
public String convertToDatabaseValue(List<ExampleEntity> 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)
Expand Down Expand Up @@ -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.")
}

}
Loading