Merge "Room Gradle Plugin" into androidx-main
diff --git a/room/integration-tests/incremental-annotation-processing/src/test/kotlin/androidx/room/gradle/RoomIncrementalAnnotationProcessingTest.kt b/room/integration-tests/incremental-annotation-processing/src/test/kotlin/androidx/room/gradle/RoomIncrementalAnnotationProcessingTest.kt
index 8720b6e..7d9085e 100644
--- a/room/integration-tests/incremental-annotation-processing/src/test/kotlin/androidx/room/gradle/RoomIncrementalAnnotationProcessingTest.kt
+++ b/room/integration-tests/incremental-annotation-processing/src/test/kotlin/androidx/room/gradle/RoomIncrementalAnnotationProcessingTest.kt
@@ -18,6 +18,8 @@
import androidx.testutils.gradle.ProjectSetupRule
import com.google.common.truth.Expect
+import java.io.File
+import java.nio.file.Files
import org.gradle.testkit.runner.BuildResult
import org.gradle.testkit.runner.GradleRunner
import org.gradle.testkit.runner.TaskOutcome
@@ -26,11 +28,6 @@
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.Parameterized
-import java.io.File
-import java.nio.file.Files
-import javax.xml.parsers.DocumentBuilderFactory
-import javax.xml.xpath.XPathConstants
-import javax.xml.xpath.XPathFactory
@RunWith(Parameterized::class)
class RoomIncrementalAnnotationProcessingTest(
@@ -110,27 +107,7 @@
* prebuilts (SNAPSHOT).
*/
private val roomVersion by lazy {
- val metadataFile = File(projectSetup.props.tipOfTreeMavenRepoPath).resolve(
- "androidx/room/room-compiler/maven-metadata.xml"
- )
- check(metadataFile.exists()) {
- "Cannot find room metadata file in ${metadataFile.absolutePath}"
- }
- check(metadataFile.isFile) {
- "Metadata file should be a file but it is not."
- }
- val xmlDoc = DocumentBuilderFactory.newInstance().newDocumentBuilder()
- .parse(metadataFile)
- val latestVersionNode = XPathFactory.newInstance().newXPath()
- .compile("/metadata/versioning/latest").evaluate(
- xmlDoc, XPathConstants.STRING
- )
- check(latestVersionNode is String) {
- """Unexpected node for latest version:
- $latestVersionNode / ${latestVersionNode::class.java}
- """.trimIndent()
- }
- latestVersionNode
+ projectSetup.getLibraryLatestVersionInLocalRepo("androidx/room/room-compiler")
}
@Before
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/DatabaseProcessingStep.kt b/room/room-compiler/src/main/kotlin/androidx/room/DatabaseProcessingStep.kt
index 472f46e..62a4e01 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/DatabaseProcessingStep.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/DatabaseProcessingStep.kt
@@ -107,6 +107,7 @@
val filename = "${db.version}.json"
val exportToResources =
Context.BooleanProcessorOptions.EXPORT_SCHEMA_RESOURCE.getValue(env)
+ val schemaInFolderPath = context.schemaInFolderPath
val schemaOutFolderPath = context.schemaOutFolderPath
if (exportToResources) {
context.logger.w(ProcessorErrors.EXPORTING_SCHEMA_TO_RESOURCES)
@@ -115,19 +116,24 @@
originatingElements = listOf(db.element)
)
db.exportSchema(schemaFileOutputStream)
- } else if (schemaOutFolderPath != null) {
+ } else if (schemaInFolderPath != null && schemaOutFolderPath != null) {
+ val schemaInFolder = SchemaFileResolver.RESOLVER.getFile(
+ Path.of(schemaInFolderPath)
+ )
val schemaOutFolder = SchemaFileResolver.RESOLVER.getFile(
Path.of(schemaOutFolderPath)
)
if (!schemaOutFolder.exists()) {
schemaOutFolder.mkdirs()
}
- val dbSchemaFolder = File(schemaOutFolder, qName)
- if (!dbSchemaFolder.exists()) {
- dbSchemaFolder.mkdirs()
+ val dbSchemaInFolder = File(schemaInFolder, qName)
+ val dbSchemaOutFolder = File(schemaOutFolder, qName)
+ if (!dbSchemaOutFolder.exists()) {
+ dbSchemaOutFolder.mkdirs()
}
db.exportSchema(
- File(dbSchemaFolder, "${db.version}.json")
+ inputFile = File(dbSchemaInFolder, "${db.version}.json"),
+ outputFile = File(dbSchemaOutFolder, "${db.version}.json")
)
} else {
context.logger.w(
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/processor/Context.kt b/room/room-compiler/src/main/kotlin/androidx/room/processor/Context.kt
index 07ee499..54969bf 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/processor/Context.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/processor/Context.kt
@@ -147,10 +147,32 @@
}
}
+ val schemaInFolderPath by lazy {
+ val internalInputFolder =
+ processingEnv.options[ProcessorOptions.INTERNAL_SCHEMA_INPUT_FOLDER.argName]
+ val legacySchemaFolder =
+ processingEnv.options[ProcessorOptions.OPTION_SCHEMA_FOLDER.argName]
+ if (!internalInputFolder.isNullOrBlank()) {
+ internalInputFolder
+ } else if (!legacySchemaFolder.isNullOrBlank()) {
+ legacySchemaFolder
+ } else {
+ null
+ }
+ }
+
val schemaOutFolderPath by lazy {
- val arg = processingEnv.options[ProcessorOptions.OPTION_SCHEMA_FOLDER.argName]
- if (arg?.isNotEmpty() == true) {
- arg
+ val internalOutputFolder =
+ processingEnv.options[ProcessorOptions.INTERNAL_SCHEMA_OUTPUT_FOLDER.argName]
+ val legacySchemaFolder =
+ processingEnv.options[ProcessorOptions.OPTION_SCHEMA_FOLDER.argName]
+ if (!internalOutputFolder.isNullOrBlank() && !legacySchemaFolder.isNullOrBlank()) {
+ logger.e(ProcessorErrors.INVALID_GRADLE_PLUGIN_AND_SCHEMA_LOCATION_OPTION)
+ }
+ if (!internalOutputFolder.isNullOrBlank()) {
+ internalOutputFolder
+ } else if (!legacySchemaFolder.isNullOrBlank()) {
+ legacySchemaFolder
} else {
null
}
@@ -256,7 +278,9 @@
}
enum class ProcessorOptions(val argName: String) {
- OPTION_SCHEMA_FOLDER("room.schemaLocation")
+ OPTION_SCHEMA_FOLDER("room.schemaLocation"),
+ INTERNAL_SCHEMA_INPUT_FOLDER("room.internal.schemaInput"),
+ INTERNAL_SCHEMA_OUTPUT_FOLDER("room.internal.schemaOutput"),
}
enum class BooleanProcessorOptions(val argName: String, private val defaultValue: Boolean) {
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/processor/DatabaseProcessor.kt b/room/room-compiler/src/main/kotlin/androidx/room/processor/DatabaseProcessor.kt
index 6e045f2..211cd37 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/processor/DatabaseProcessor.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/processor/DatabaseProcessor.kt
@@ -28,7 +28,7 @@
import androidx.room.migration.bundle.DatabaseBundle
import androidx.room.migration.bundle.SchemaBundle
import androidx.room.processor.ProcessorErrors.AUTO_MIGRATION_FOUND_BUT_EXPORT_SCHEMA_OFF
-import androidx.room.processor.ProcessorErrors.AUTO_MIGRATION_SCHEMA_OUT_FOLDER_NULL
+import androidx.room.processor.ProcessorErrors.AUTO_MIGRATION_SCHEMA_IN_FOLDER_NULL
import androidx.room.processor.ProcessorErrors.autoMigrationSchemasMustBeRoomGenerated
import androidx.room.processor.ProcessorErrors.invalidAutoMigrationSchema
import androidx.room.util.SchemaFileResolver
@@ -151,63 +151,65 @@
val autoMigrationList = dbAnnotation
.getAsAnnotationBoxArray<AutoMigration>("autoMigrations")
+ if (autoMigrationList.isEmpty()) {
+ return emptyList()
+ }
- if (autoMigrationList.isNotEmpty()) {
- if (!dbAnnotation.value.exportSchema) {
- context.logger.e(
- element,
- AUTO_MIGRATION_FOUND_BUT_EXPORT_SCHEMA_OFF
- )
- return emptyList()
- }
- if (context.schemaOutFolderPath == null) {
- context.logger.e(
- element,
- AUTO_MIGRATION_SCHEMA_OUT_FOLDER_NULL
- )
- return emptyList()
- }
+ if (!dbAnnotation.value.exportSchema) {
+ context.logger.e(
+ element,
+ AUTO_MIGRATION_FOUND_BUT_EXPORT_SCHEMA_OFF
+ )
+ return emptyList()
+ }
+ val schemaInFolderPath = context.schemaInFolderPath
+ if (schemaInFolderPath == null) {
+ context.logger.e(
+ element,
+ AUTO_MIGRATION_SCHEMA_IN_FOLDER_NULL
+ )
+ return emptyList()
}
return autoMigrationList.mapNotNull {
- val databaseSchemaFolderPath = Path.of(
- context.schemaOutFolderPath!!,
+ val databaseSchemaInFolderPath = Path.of(
+ schemaInFolderPath,
element.asClassName().canonicalName
)
val autoMigration = it.value
val validatedFromSchemaFile = getValidatedSchemaFile(
autoMigration.from,
- databaseSchemaFolderPath
- )
+ databaseSchemaInFolderPath
+ ) ?: return@mapNotNull null
- fun deserializeSchemaFile(fileInputStream: FileInputStream, versionNumber: Int): Any {
+ fun deserializeSchemaFile(
+ fileInputStream: FileInputStream,
+ versionNumber: Int
+ ): DatabaseBundle? {
return try {
SchemaBundle.deserialize(fileInputStream).database
} catch (th: Throwable) {
invalidAutoMigrationSchema(
"$versionNumber.json",
- databaseSchemaFolderPath.toString()
+ databaseSchemaInFolderPath.toString()
)
+ null
}
}
- if (validatedFromSchemaFile != null) {
- val fromSchemaBundle = validatedFromSchemaFile.inputStream().use {
- deserializeSchemaFile(it, autoMigration.from)
- }
- val toSchemaBundle = if (autoMigration.to == latestDbSchema.version) {
+ val fromSchemaBundle = validatedFromSchemaFile.inputStream().use {
+ deserializeSchemaFile(it, autoMigration.from)
+ }
+ val toSchemaBundle =
+ if (autoMigration.to == latestDbSchema.version) {
latestDbSchema
} else {
val validatedToSchemaFile = getValidatedSchemaFile(
autoMigration.to,
- databaseSchemaFolderPath
- )
- if (validatedToSchemaFile != null) {
- validatedToSchemaFile.inputStream().use {
- deserializeSchemaFile(it, autoMigration.to)
- }
- } else {
- return@mapNotNull null
+ databaseSchemaInFolderPath
+ ) ?: return@mapNotNull null
+ validatedToSchemaFile.inputStream().use {
+ deserializeSchemaFile(it, autoMigration.to)
}
}
if (fromSchemaBundle !is DatabaseBundle || toSchemaBundle !is DatabaseBundle) {
@@ -221,15 +223,12 @@
return@mapNotNull null
}
- AutoMigrationProcessor(
- context = context,
- spec = it.getAsType("spec")!!,
- fromSchemaBundle = fromSchemaBundle,
- toSchemaBundle = toSchemaBundle
- ).process()
- } else {
- null
- }
+ AutoMigrationProcessor(
+ context = context,
+ spec = it.getAsType("spec")!!,
+ fromSchemaBundle = fromSchemaBundle,
+ toSchemaBundle = toSchemaBundle
+ ).process()
}
}
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/processor/ProcessorErrors.kt b/room/room-compiler/src/main/kotlin/androidx/room/processor/ProcessorErrors.kt
index 0367eeb..1612b0c 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/processor/ProcessorErrors.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/processor/ProcessorErrors.kt
@@ -539,9 +539,10 @@
""".trim()
}
- val MISSING_SCHEMA_EXPORT_DIRECTORY = "Schema export directory is not provided to the" +
- " annotation processor so we cannot export the schema. You can either provide" +
- " `room.schemaLocation` annotation processor argument OR set exportSchema to false."
+ val MISSING_SCHEMA_EXPORT_DIRECTORY = "Schema export directory was not provided to the" +
+ " annotation processor so Room cannot export the schema. You can either provide" +
+ " `room.schemaLocation` annotation processor argument by applying the Room Gradle plugin" +
+ " (id 'androidx.room') OR set exportSchema to false."
val INVALID_FOREIGN_KEY_ACTION = "Invalid foreign key action. It must be one of the constants" +
" defined in ForeignKey.Action"
@@ -938,8 +939,11 @@
fun invalidAutoMigrationSchema(schemaFile: String, schemaOutFolderPath: String): String {
return "Found invalid schema file '$schemaFile.json' at the schema out " +
- "folder: $schemaOutFolderPath. The schema files must be generated by Room. Cannot " +
- "generate auto migrations."
+ "folder: $schemaOutFolderPath.\nIf you've modified the file, you might've broken the " +
+ "JSON format, try deleting the file and re-running the compiler.\n" +
+ "If you've not modified the file, please file a bug at " +
+ "https://issuetracker.google.com/issues/new?component=413107&template=1096568 " +
+ "with a sample app to reproduce the issue."
}
fun autoMigrationSchemasMustBeRoomGenerated(
@@ -1076,13 +1080,13 @@
return "Conflicting @RenameColumn annotations found: [$annotations]"
}
- val AUTO_MIGRATION_FOUND_BUT_EXPORT_SCHEMA_OFF = "Cannot create auto migrations when export " +
- "schema is OFF."
+ val AUTO_MIGRATION_FOUND_BUT_EXPORT_SCHEMA_OFF = "Cannot create auto migrations when " +
+ "exportSchema is false."
- val AUTO_MIGRATION_SCHEMA_OUT_FOLDER_NULL = "Schema export directory is not provided to the" +
- " annotation processor so we cannot import the schema. To generate auto migrations, you " +
- "must provide `room.schemaLocation` annotation processor argument AND set exportSchema to" +
- " true."
+ val AUTO_MIGRATION_SCHEMA_IN_FOLDER_NULL = "Schema import directory was not provided to the" +
+ " annotation processor so Room cannot read older schemas. To generate auto migrations," +
+ " you must provide `room.schemaLocation` annotation processor arguments by applying the" +
+ " Room Gradle plugin (id 'androidx.room') AND set exportSchema to true."
fun tableWithConflictingPrefixFound(tableName: String): String {
return "The new version of the schema contains '$tableName' a table name" +
@@ -1157,4 +1161,10 @@
" the schema file and extracting it from the JAR but not for production builds, otherwise" +
" the schema file will end up in the final artifact which is typically not desired. This" +
" warning serves as a reminder to use room.exportSchemaResource cautiously."
+
+ val INVALID_GRADLE_PLUGIN_AND_SCHEMA_LOCATION_OPTION = "The Room Gradle plugin " +
+ "(id 'androidx.room') cannot be used with an explicit use of the annotation processor" +
+ "option `room.schemaLocation`, please remove the configuration of the option and " +
+ "configure the schema location via the plugin project extension: " +
+ "`room { schemaDirectory(...) }`."
}
\ No newline at end of file
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/util/SchemaFileResolver.kt b/room/room-compiler/src/main/kotlin/androidx/room/util/SchemaFileResolver.kt
index b97d038..6cd884e 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/util/SchemaFileResolver.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/util/SchemaFileResolver.kt
@@ -28,8 +28,8 @@
/**
* Resolves the given path to a file. The path will be a either a sibling of Room's schema
- * location or the folder itself as provided via the annotation processor option
- * 'room.schemaLocation'.
+ * location or the folder itself as provided via the annotation processor options
+ * 'room.schemaLocation' or 'roomSchemaInput.
*/
fun getFile(path: Path): File
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/vo/Database.kt b/room/room-compiler/src/main/kotlin/androidx/room/vo/Database.kt
index 1bcd843b..6674a85 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/vo/Database.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/vo/Database.kt
@@ -101,32 +101,26 @@
DigestUtils.md5Hex(input)
}
- fun exportSchema(file: File) {
+ // Writes scheme file to output file, using the input file to check if the schema has changed
+ // otherwise it is not written.
+ fun exportSchema(inputFile: File, outputFile: File) {
val schemaBundle = SchemaBundle(SchemaBundle.LATEST_FORMAT, bundle)
- if (file.exists()) {
- val existing = try {
- file.inputStream().use {
- SchemaBundle.deserialize(it)
- }
- } catch (th: Throwable) {
- throw IllegalStateException(
- """
- Cannot parse existing schema file: ${file.absolutePath}.
- If you've modified the file, you might've broken the JSON format, try
- deleting the file and re-running the compiler.
- If you've not modified the file, please file a bug at
- https://issuetracker.google.com/issues/new?component=413107&template=1096568
- with a sample app to reproduce the issue.
- """.trimIndent()
- )
+ if (inputFile.exists()) {
+ val existing = inputFile.inputStream().use {
+ SchemaBundle.deserialize(it)
}
+ // If existing schema file is the same as the current schema then do not write the file
+ // which helps the copy task configured by the Room Gradle Plugin skip execution due
+ // to empty variant schema output directory.
if (existing.isSchemaEqual(schemaBundle)) {
return
}
}
- SchemaBundle.serialize(schemaBundle, file)
+ SchemaBundle.serialize(schemaBundle, outputFile)
}
+ // Writes scheme file to output stream, the stream should be for a resource otherwise use the
+ // file version of `exportSchema`.
fun exportSchema(outputStream: OutputStream) {
val schemaBundle = SchemaBundle(SchemaBundle.LATEST_FORMAT, bundle)
SchemaBundle.serialize(schemaBundle, outputStream)
diff --git a/room/room-gradle-plugin/build.gradle b/room/room-gradle-plugin/build.gradle
new file mode 100644
index 0000000..8b12c66
--- /dev/null
+++ b/room/room-gradle-plugin/build.gradle
@@ -0,0 +1,92 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import androidx.build.LibraryType
+import androidx.build.SdkResourceGenerator
+
+plugins {
+ id("AndroidXPlugin")
+ id("kotlin")
+ id("java-gradle-plugin")
+}
+
+configurations {
+ // Config for plugin classpath to be used during tests
+ testPlugin {
+ canBeConsumed = false
+ canBeResolved = true
+ }
+}
+
+dependencies {
+ implementation(libs.kotlinStdlib)
+ implementation(gradleApi())
+ implementation("com.android.tools.build:gradle:7.3.0")
+ compileOnly(libs.kotlinGradlePluginz)
+ compileOnly(libs.kspGradlePluginz)
+
+ testImplementation(project(":internal-testutils-gradle-plugin"))
+ testImplementation(gradleTestKit())
+ testImplementation(libs.junit)
+ testImplementation(libs.truth)
+ testImplementation(libs.testParameterInjector)
+
+ testPlugin(libs.kotlinGradlePluginz)
+ testPlugin(libs.kspGradlePluginz)
+}
+
+SdkResourceGenerator.generateForHostTest(project)
+
+// Configure the generating task of plugin-under-test-metadata.properties to
+// include additional dependencies for the injected plugin classpath that
+// are not present in the main runtime dependencies. This allows us to test
+// the KAPT / KSP plugins while keeping a compileOnly dep on the main source.
+tasks.withType(PluginUnderTestMetadata.class).named("pluginUnderTestMetadata").configure {
+ it.pluginClasspath.from(configurations.testPlugin)
+}
+
+// Configure publishing tasks to be dependencies of 'test' so those artifacts are available for
+// the test project executed with Gradle Test Kit.
+tasks.findByPath("test").dependsOn(
+ tasks.findByPath(":annotation:annotation-experimental:publish"),
+ tasks.findByPath(":room:room-common:publish"),
+ tasks.findByPath(":room:room-runtime:publish"),
+ tasks.findByPath(":room:room-migration:publish"),
+ tasks.findByPath(":room:room-compiler:publish"),
+ tasks.findByPath(":room:room-compiler-processing:publish"),
+ tasks.findByPath(":sqlite:sqlite:publish"),
+ tasks.findByPath(":sqlite:sqlite-framework:publish"),
+)
+
+gradlePlugin {
+ plugins {
+ room {
+ id = "androidx.room"
+ implementationClass = "androidx.room.gradle.RoomGradlePlugin"
+ }
+ }
+}
+
+androidx {
+ name = "Android Room Gradle Plugin"
+ type = LibraryType.GRADLE_PLUGIN
+ inceptionYear = "2023"
+ description = "Android Room Gradle Plugin"
+}
+
+validatePlugins {
+ enableStricterValidation = true
+}
\ No newline at end of file
diff --git a/room/room-gradle-plugin/src/main/java/androidx/room/gradle/RoomExtension.kt b/room/room-gradle-plugin/src/main/java/androidx/room/gradle/RoomExtension.kt
new file mode 100644
index 0000000..dc10b3c
--- /dev/null
+++ b/room/room-gradle-plugin/src/main/java/androidx/room/gradle/RoomExtension.kt
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.room.gradle
+
+import javax.inject.Inject
+import org.gradle.api.provider.Provider
+import org.gradle.api.provider.ProviderFactory
+
+open class RoomExtension @Inject constructor(private val providers: ProviderFactory) {
+ internal var schemaDirectory: Provider<String>? = null
+
+ // TODO(b/279748243): Consider adding overload that takes `org.gradle.api.file.Director`.
+
+ /**
+ * Sets the schema location where Room will output exported schema files.
+ *
+ * The location specified will be used as the base directory for schema files that will be
+ * generated per build variant. i.e. for a 'debug' build of the product flavor 'free' then a
+ * schema will be generated in
+ * `<schemaDirectory>/freeDebug/<database-package>/<database-version>.json`.
+ *
+ * See [Export Schemas Documentation](https://developer.android.com/training/data-storage/room/migrating-db-versions#export-schemas)
+ */
+ open fun schemaDirectory(path: String) {
+ schemaDirectory(providers.provider { path })
+ }
+
+ /**
+ * Sets the schema location where Room will output exported schema files.
+ *
+ * The location specified will be used as the base directory for schema files that will be
+ * generated per build variant. i.e. for a 'debug' build of the product flavor 'free' then a
+ * schema will be generated in
+ * `<schemaDirectory>/freeDebug/<database-package>/<database-version>.json`.
+ *
+ * See [Export Schemas Documentation](https://developer.android.com/training/data-storage/room/migrating-db-versions#export-schemas)
+ */
+ open fun schemaDirectory(path: Provider<String>) {
+ schemaDirectory = path
+ }
+}
\ No newline at end of file
diff --git a/room/room-gradle-plugin/src/main/java/androidx/room/gradle/RoomGradlePlugin.kt b/room/room-gradle-plugin/src/main/java/androidx/room/gradle/RoomGradlePlugin.kt
new file mode 100644
index 0000000..9dacb6d
--- /dev/null
+++ b/room/room-gradle-plugin/src/main/java/androidx/room/gradle/RoomGradlePlugin.kt
@@ -0,0 +1,301 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.room.gradle
+
+import com.android.build.api.AndroidPluginVersion
+import com.android.build.api.variant.AndroidComponentsExtension
+import com.android.build.api.variant.ComponentIdentity
+import com.android.build.api.variant.Variant
+import com.android.build.gradle.api.AndroidBasePlugin
+import com.google.devtools.ksp.gradle.KspTaskJvm
+import java.util.Locale
+import javax.inject.Inject
+import kotlin.contracts.ExperimentalContracts
+import kotlin.contracts.contract
+import kotlin.io.path.Path
+import kotlin.io.path.notExists
+import org.gradle.api.DefaultTask
+import org.gradle.api.GradleException
+import org.gradle.api.Plugin
+import org.gradle.api.Project
+import org.gradle.api.Task
+import org.gradle.api.file.ConfigurableFileCollection
+import org.gradle.api.file.Directory
+import org.gradle.api.file.DirectoryProperty
+import org.gradle.api.file.ProjectLayout
+import org.gradle.api.model.ObjectFactory
+import org.gradle.api.provider.Provider
+import org.gradle.api.tasks.IgnoreEmptyDirectories
+import org.gradle.api.tasks.Input
+import org.gradle.api.tasks.InputFiles
+import org.gradle.api.tasks.Internal
+import org.gradle.api.tasks.OutputDirectory
+import org.gradle.api.tasks.PathSensitive
+import org.gradle.api.tasks.PathSensitivity
+import org.gradle.api.tasks.SkipWhenEmpty
+import org.gradle.api.tasks.TaskAction
+import org.gradle.api.tasks.TaskProvider
+import org.gradle.api.tasks.compile.JavaCompile
+import org.gradle.configurationcache.extensions.capitalized
+import org.gradle.process.CommandLineArgumentProvider
+import org.gradle.work.DisableCachingByDefault
+import org.jetbrains.kotlin.gradle.internal.KaptTask
+
+class RoomGradlePlugin @Inject constructor(
+ private val projectLayout: ProjectLayout,
+ private val objectFactory: ObjectFactory,
+) : Plugin<Project> {
+ override fun apply(project: Project) {
+ var configured = false
+ project.plugins.withType(AndroidBasePlugin::class.java) {
+ configured = true
+ configureRoom(project)
+ }
+ project.afterEvaluate {
+ project.check(configured) {
+ "The Room Gradle plugin can only be applied to an Android project."
+ }
+ }
+ }
+
+ private fun configureRoom(project: Project) {
+ // TODO(b/277899741): Validate version of Room supports the AP options configured by plugin.
+ val roomExtension =
+ project.extensions.create("room", RoomExtension::class.java)
+ val componentsExtension =
+ project.extensions.findByType(AndroidComponentsExtension::class.java)
+ project.check(componentsExtension != null) {
+ "Could not find the Android Gradle Plugin (AGP) extension, the Room Gradle plugin " +
+ "should be only applied to an Android projects."
+ }
+ project.check(componentsExtension.pluginVersion >= AndroidPluginVersion(7, 3)) {
+ "The Room Gradle plugin is only compatible with Android Gradle plugin (AGP) " +
+ "version 7.3.0 or higher (found ${componentsExtension.pluginVersion})."
+ }
+ componentsExtension.onVariants { variant ->
+ val locationProvider = roomExtension.schemaDirectory
+ project.check(locationProvider != null) {
+ "The Room Gradle plugin was applied but not schema location was specified. " +
+ "Use the `room { schemaDirectory(...) }` DSL to specify one."
+ }
+ val schemaDirectory = locationProvider.get()
+ project.check(schemaDirectory.isNotEmpty()) {
+ "The schemaDirectory path must not be empty."
+ }
+ configureVariant(project, schemaDirectory, variant)
+ }
+ }
+
+ private fun configureVariant(
+ project: Project,
+ schemaDirectory: String,
+ variant: Variant
+ ) {
+ val androidVariantTaskNames = AndroidVariantsTaskNames(variant.name, variant)
+ val configureTask: (Task, ComponentIdentity) -> RoomSchemaDirectoryArgumentProvider = {
+ task, variantIdentity ->
+ val schemaDirectoryPath = Path(schemaDirectory, variantIdentity.name)
+ if (schemaDirectoryPath.notExists()) {
+ project.check(schemaDirectoryPath.toFile().mkdirs()) {
+ "Unable to create directory: $schemaDirectoryPath"
+ }
+ }
+ val schemaInputDir = objectFactory.directoryProperty().apply {
+ set(project.file(schemaDirectoryPath))
+ }
+
+ val schemaOutputDir =
+ projectLayout.buildDirectory.dir("intermediates/room/schemas/${task.name}")
+
+ val copyTask = androidVariantTaskNames.copyTasks.getOrPut(variant.name) {
+ project.tasks.register(
+ "copyRoomSchemas${variantIdentity.name.capitalize()}",
+ RoomSchemaCopyTask::class.java
+ ) {
+ it.schemaDirectory.set(schemaInputDir)
+ }
+ }
+ copyTask.configure { it.variantSchemaOutputDirectories.from(schemaOutputDir) }
+ task.finalizedBy(copyTask)
+
+ RoomSchemaDirectoryArgumentProvider(
+ forKsp = task.isKspTask(),
+ schemaInputDir = schemaInputDir,
+ schemaOutputDir = schemaOutputDir
+ )
+ }
+
+ configureJavaTasks(project, androidVariantTaskNames, configureTask)
+ configureKaptTasks(project, androidVariantTaskNames, configureTask)
+ configureKspTasks(project, androidVariantTaskNames, configureTask)
+
+ // TODO: Consider also setting up the androidTest and test source set to include the
+ // relevant schema location so users can use MigrationTestHelper without additional
+ // configuration.
+ }
+
+ private fun configureJavaTasks(
+ project: Project,
+ androidVariantsTaskNames: AndroidVariantsTaskNames,
+ configureBlock: (Task, ComponentIdentity) -> RoomSchemaDirectoryArgumentProvider
+ ) = project.tasks.withType(JavaCompile::class.java) { task ->
+ androidVariantsTaskNames.withJavaCompile(task.name)?.let { variantIdentity ->
+ val argProvider = configureBlock.invoke(task, variantIdentity)
+ task.options.compilerArgumentProviders.add(argProvider)
+ }
+ }
+
+ private fun configureKaptTasks(
+ project: Project,
+ androidVariantsTaskNames: AndroidVariantsTaskNames,
+ configureBlock: (Task, ComponentIdentity) -> RoomSchemaDirectoryArgumentProvider
+ ) = project.plugins.withId("kotlin-kapt") {
+ project.tasks.withType(KaptTask::class.java) { task ->
+ androidVariantsTaskNames.withKaptTask(task.name)?.let { variantIdentity ->
+ val argProvider = configureBlock.invoke(task, variantIdentity)
+ // TODO: Update once KT-58009 is fixed.
+ try {
+ // Because of KT-58009, we need to add a `listOf(argProvider)` instead
+ // of `argProvider`.
+ task.annotationProcessorOptionProviders.add(listOf(argProvider))
+ } catch (e: Throwable) {
+ // Once KT-58009 is fixed, adding `listOf(argProvider)` will fail, we will
+ // pass `argProvider` instead, which is the correct way.
+ task.annotationProcessorOptionProviders.add(argProvider)
+ }
+ }
+ }
+ }
+
+ private fun configureKspTasks(
+ project: Project,
+ androidVariantsTaskNames: AndroidVariantsTaskNames,
+ configureBlock: (Task, ComponentIdentity) -> RoomSchemaDirectoryArgumentProvider
+ ) = project.plugins.withId("com.google.devtools.ksp") {
+ project.tasks.withType(KspTaskJvm::class.java) { task ->
+ androidVariantsTaskNames.withKspTaskJvm(task.name)?.let { variantIdentity ->
+ val argProvider = configureBlock.invoke(task, variantIdentity)
+ task.commandLineArgumentProviders.add(argProvider)
+ }
+ }
+ }
+
+ internal class AndroidVariantsTaskNames(
+ private val variantName: String,
+ private val variantIdentity: ComponentIdentity
+ ) {
+ // Variant name to copy task
+ val copyTasks = mutableMapOf<String, TaskProvider<RoomSchemaCopyTask>>()
+
+ private val javaCompileName by lazy {
+ "compile${variantName.capitalized()}JavaWithJavac"
+ }
+
+ private val kaptTaskName by lazy {
+ "kapt${variantName.capitalized()}Kotlin"
+ }
+
+ private val kspTaskJvm by lazy {
+ "ksp${variantName.capitalized()}Kotlin"
+ }
+
+ fun withJavaCompile(taskName: String) =
+ if (taskName == javaCompileName) variantIdentity else null
+
+ fun withKaptTask(taskName: String) =
+ if (taskName == kaptTaskName) variantIdentity else null
+
+ fun withKspTaskJvm(taskName: String) =
+ if (taskName == kspTaskJvm) variantIdentity else null
+ }
+
+ @DisableCachingByDefault(because = "Simple disk bound task.")
+ abstract class RoomSchemaCopyTask : DefaultTask() {
+ @get:InputFiles
+ @get:SkipWhenEmpty
+ @get:IgnoreEmptyDirectories
+ @get:PathSensitive(PathSensitivity.RELATIVE)
+ abstract val variantSchemaOutputDirectories: ConfigurableFileCollection
+
+ @get:Internal
+ abstract val schemaDirectory: DirectoryProperty
+
+ @TaskAction
+ fun copySchemas() {
+ variantSchemaOutputDirectories.files
+ .filter { it.exists() }
+ .forEach {
+ // TODO(b/278266663): Error when two same relative path schemas are found in out
+ // dirs and their content is different an indicator of an inconsistency between
+ // the compile tasks of the same variant.
+ it.copyRecursively(schemaDirectory.get().asFile, overwrite = true)
+ }
+ }
+ }
+
+ class RoomSchemaDirectoryArgumentProvider(
+ @get:Input
+ val forKsp: Boolean,
+ @get:InputFiles
+ @get:PathSensitive(PathSensitivity.RELATIVE)
+ val schemaInputDir: Provider<Directory>,
+ @get:OutputDirectory
+ val schemaOutputDir: Provider<Directory>
+ ) : CommandLineArgumentProvider {
+ override fun asArguments() = buildList {
+ val prefix = if (forKsp) "" else "-A"
+ add("${prefix}room.internal.schemaInput=${schemaInputDir.get().asFile.path}")
+ add("${prefix}room.internal.schemaOutput=${schemaOutputDir.get().asFile.path}")
+ }
+ }
+
+ companion object {
+ internal fun String.capitalize(): String = this.replaceFirstChar {
+ if (it.isLowerCase()) it.titlecase(Locale.US) else it.toString()
+ }
+
+ internal fun Task.isKspTask(): Boolean = try {
+ val kspTaskClass = Class.forName("com.google.devtools.ksp.gradle.KspTask")
+ kspTaskClass.isAssignableFrom(this::class.java)
+ } catch (ex: ClassNotFoundException) {
+ false
+ }
+
+ @OptIn(ExperimentalContracts::class)
+ internal fun Project.check(value: Boolean, lazyMessage: () -> String) {
+ contract {
+ returns() implies value
+ }
+ if (isGradleSyncRunning()) return
+ if (!value) {
+ throw GradleException(lazyMessage())
+ }
+ }
+
+ private fun Project.isGradleSyncRunning() = gradleSyncProps.any {
+ it in this.properties && this.properties[it].toString().toBoolean()
+ }
+
+ private val gradleSyncProps by lazy {
+ listOf(
+ "android.injected.build.model.v2",
+ "android.injected.build.model.only",
+ "android.injected.build.model.only.advanced",
+ )
+ }
+ }
+}
\ No newline at end of file
diff --git a/room/room-gradle-plugin/src/test/java/androidx/room/gradle/RoomGradlePluginTest.kt b/room/room-gradle-plugin/src/test/java/androidx/room/gradle/RoomGradlePluginTest.kt
new file mode 100644
index 0000000..ce82693
--- /dev/null
+++ b/room/room-gradle-plugin/src/test/java/androidx/room/gradle/RoomGradlePluginTest.kt
@@ -0,0 +1,356 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.room.gradle
+
+import androidx.testutils.gradle.ProjectSetupRule
+import com.google.common.truth.Truth.assertThat
+import com.google.testing.junit.testparameterinjector.TestParameter
+import com.google.testing.junit.testparameterinjector.TestParameterInjector
+import java.io.File
+import org.gradle.testkit.runner.BuildResult
+import org.gradle.testkit.runner.GradleRunner
+import org.gradle.testkit.runner.TaskOutcome
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(TestParameterInjector::class)
+class RoomGradlePluginTest(
+ @TestParameter val backend: ProcessingBackend
+) {
+ @get:Rule
+ val projectSetup = ProjectSetupRule()
+
+ private val roomVersion by lazy {
+ projectSetup.getLibraryLatestVersionInLocalRepo("androidx/room/room-compiler")
+ }
+
+ private fun setup(projectName: String, projectRoot: File = projectSetup.rootDir) {
+ // copy test project
+ File("src/test/test-data/$projectName").copyRecursively(projectRoot)
+
+ if (backend.isForKotlin) {
+ // copy Kotlin database file
+ File("src/test/test-data/kotlin/MyDatabase.kt").let {
+ it.copyTo(projectRoot.resolve("src/main/java/room/testapp/${it.name}"))
+ }
+ } else {
+ // copy Java database file
+ File("src/test/test-data/java/MyDatabase.java").let {
+ it.copyTo(projectRoot.resolve("src/main/java/room/testapp/${it.name}"))
+ }
+ }
+
+ val additionalPluginsBlock = when (backend) {
+ ProcessingBackend.JAVAC ->
+ ""
+ ProcessingBackend.KAPT ->
+ """
+ id('kotlin-android')
+ id('kotlin-kapt')
+ """
+ ProcessingBackend.KSP ->
+ """
+ id('kotlin-android')
+ id('com.google.devtools.ksp')
+ """
+ }
+
+ val repositoriesBlock = buildString {
+ appendLine("repositories {")
+ projectSetup.allRepositoryPaths.forEach {
+ appendLine("""maven { url "$it" }""")
+ }
+ appendLine("}")
+ }
+
+ val processorConfig = when (backend) {
+ ProcessingBackend.JAVAC -> "annotationProcessor"
+ ProcessingBackend.KAPT -> "kapt"
+ ProcessingBackend.KSP -> "ksp"
+ }
+
+ val kotlinJvmTargetBlock = if (backend.isForKotlin) {
+ """
+ tasks.withType(
+ org.jetbrains.kotlin.gradle.tasks.KotlinCompile
+ ).configureEach {
+ kotlinOptions {
+ jvmTarget = "1.8"
+ }
+ }
+ """.trimIndent()
+ } else {
+ ""
+ }
+
+ // set up build file
+ File(projectRoot, "build.gradle").writeText(
+ """
+ plugins {
+ id('com.android.application')
+ id('androidx.room')
+ $additionalPluginsBlock
+ }
+
+ $repositoriesBlock
+
+ %s
+
+ dependencies {
+ // Uses latest Room built from tip of tree
+ implementation "androidx.room:room-runtime:$roomVersion"
+ $processorConfig "androidx.room:room-compiler:$roomVersion"
+ }
+
+ android {
+ namespace "room.testapp"
+ compileOptions {
+ sourceCompatibility = JavaVersion.VERSION_1_8
+ targetCompatibility = JavaVersion.VERSION_1_8
+ }
+ }
+
+ $kotlinJvmTargetBlock
+
+ room {
+ schemaDirectory("${'$'}projectDir/schemas")
+ }
+
+ """
+ .trimMargin()
+ // doing format instead of "$projectSetup.androidProject" on purpose,
+ // because otherwise trimIndent will mess with formatting
+ .format(projectSetup.androidProject)
+
+ )
+ }
+
+ @Test
+ fun testWorkflow() {
+ setup("simple-project")
+
+ // First clean build, all tasks need to run
+ runGradleTasks(CLEAN_TASK, COMPILE_TASK).let { result ->
+ result.assertTaskOutcome(COMPILE_TASK, TaskOutcome.SUCCESS)
+ result.assertTaskOutcome(COPY_TASK, TaskOutcome.SUCCESS)
+ }
+
+ // Schema file at version 1 is created
+ var schemaOneTimestamp: Long
+ projectSetup.rootDir.resolve("schemas/debug/room.testapp.MyDatabase/1.json").let {
+ assertThat(it.exists()).isTrue()
+ schemaOneTimestamp = it.lastModified()
+ }
+
+ // Incremental build, compile task re-runs because schema 1 is used as input, but no copy
+ // is done since schema has not changed.
+ runGradleTasks(COMPILE_TASK).let { result ->
+ result.assertTaskOutcome(COMPILE_TASK, TaskOutcome.SUCCESS)
+ result.assertTaskOutcome(COPY_TASK, TaskOutcome.NO_SOURCE)
+ }
+
+ // Incremental build, everything is up to date.
+ runGradleTasks(COMPILE_TASK).let { result ->
+ result.assertTaskOutcome(COMPILE_TASK, TaskOutcome.UP_TO_DATE)
+ result.assertTaskOutcome(COPY_TASK, TaskOutcome.NO_SOURCE)
+ }
+
+ // Make a change that changes the schema at version 1
+ searchAndReplace(
+ file = projectSetup.rootDir.resolve("src/main/java/room/testapp/MyEntity.java"),
+ search = "// Insert-change",
+ replace = "public String text;"
+ )
+
+ // Incremental build, new schema for version 1 is generated and copied.
+ runGradleTasks(COMPILE_TASK).let { result ->
+ result.assertTaskOutcome(COMPILE_TASK, TaskOutcome.SUCCESS)
+ result.assertTaskOutcome(COPY_TASK, TaskOutcome.SUCCESS)
+ }
+
+ // Check schema file at version 1 is updated
+ projectSetup.rootDir.resolve("schemas/debug/room.testapp.MyDatabase/1.json").let {
+ assertThat(it.exists()).isTrue()
+ assertThat(schemaOneTimestamp).isNotEqualTo(it.lastModified())
+ schemaOneTimestamp = it.lastModified()
+ }
+
+ // Incremental build, compile task re-runs because schema 1 is used as input (it changed),
+ // but no copy is done since schema has not changed.
+ runGradleTasks(COMPILE_TASK).let { result ->
+ result.assertTaskOutcome(COMPILE_TASK, TaskOutcome.SUCCESS)
+ result.assertTaskOutcome(COPY_TASK, TaskOutcome.NO_SOURCE)
+ }
+
+ // Incremental build, everything is up to date.
+ runGradleTasks(COMPILE_TASK).let { result ->
+ result.assertTaskOutcome(COMPILE_TASK, TaskOutcome.UP_TO_DATE)
+ result.assertTaskOutcome(COPY_TASK, TaskOutcome.NO_SOURCE)
+ }
+
+ // Add a new file, it does not change the schema
+ projectSetup.rootDir.resolve("src/main/java/room/testapp/NewUtil.java")
+ .writeText("""
+ package room.testapp;
+ public class NewUtil {
+ }
+ """.trimIndent())
+
+ // Incremental build, compile task re-runs because of new source, but no schema is copied
+ // since Room processor didn't even run.
+ runGradleTasks(COMPILE_TASK).let { result ->
+ result.assertTaskOutcome(COMPILE_TASK, TaskOutcome.SUCCESS)
+ result.assertTaskOutcome(COPY_TASK, TaskOutcome.NO_SOURCE)
+ }
+
+ // Incremental build, everything is up to date.
+ runGradleTasks(COMPILE_TASK).let { result ->
+ result.assertTaskOutcome(COMPILE_TASK, TaskOutcome.UP_TO_DATE)
+ result.assertTaskOutcome(COPY_TASK, TaskOutcome.NO_SOURCE)
+ }
+
+ // Change the database version to 2
+ val dbFile = if (backend.isForKotlin) "MyDatabase.kt" else "MyDatabase.java"
+ searchAndReplace(
+ file = projectSetup.rootDir.resolve("src/main/java/room/testapp/$dbFile"),
+ search = "version = 1",
+ replace = "version = 2"
+ )
+
+ // Incremental build, due to the version change a new schema file is generated.
+ runGradleTasks(COMPILE_TASK).let { result ->
+ result.assertTaskOutcome(COMPILE_TASK, TaskOutcome.SUCCESS)
+ result.assertTaskOutcome(COPY_TASK, TaskOutcome.SUCCESS)
+ }
+
+ // Check schema file at version 1 is still present and unchanged.
+ projectSetup.rootDir.resolve("schemas/debug/room.testapp.MyDatabase/1.json").let {
+ assertThat(it.exists()).isTrue()
+ assertThat(schemaOneTimestamp).isEqualTo(it.lastModified())
+ }
+
+ // Check schema file at version 2 is created and copied.
+ projectSetup.rootDir.resolve("schemas/debug/room.testapp.MyDatabase/2.json").let {
+ assertThat(it.exists()).isTrue()
+ }
+ }
+
+ @Test
+ fun testFlavoredProject() {
+ setup("flavored-project")
+
+ File(projectSetup.rootDir, "build.gradle").appendText(
+ """
+ android {
+ flavorDimensions "mode"
+ productFlavors {
+ flavorOne {
+ dimension "mode"
+ }
+ flavorTwo {
+ dimension "mode"
+ }
+ }
+ }
+ """.trimIndent()
+ )
+
+ runGradleTasks(
+ CLEAN_TASK,
+ "compileFlavorOneDebugJavaWithJavac",
+ "compileFlavorTwoDebugJavaWithJavac"
+ ).let { result ->
+ result.assertTaskOutcome(":compileFlavorOneDebugJavaWithJavac", TaskOutcome.SUCCESS)
+ result.assertTaskOutcome(":compileFlavorTwoDebugJavaWithJavac", TaskOutcome.SUCCESS)
+ result.assertTaskOutcome(":copyRoomSchemasFlavorOneDebug", TaskOutcome.SUCCESS)
+ result.assertTaskOutcome(":copyRoomSchemasFlavorTwoDebug", TaskOutcome.SUCCESS)
+ }
+ // Check schema files are generated for both flavor, each in its own folder.
+ val flavorOneSchema = projectSetup.rootDir.resolve(
+ "schemas/flavorOneDebug/room.testapp.MyDatabase/1.json"
+ )
+ val flavorTwoSchema = projectSetup.rootDir.resolve(
+ "schemas/flavorTwoDebug/room.testapp.MyDatabase/1.json"
+ )
+ assertThat(flavorOneSchema.exists()).isTrue()
+ assertThat(flavorTwoSchema.exists()).isTrue()
+ // Check the schemas in both flavors are different
+ assertThat(flavorOneSchema.readText()).isNotEqualTo(flavorTwoSchema.readText())
+ }
+
+ @Test
+ fun testMoreBuildTypesProject() {
+ setup("simple-project")
+
+ File(projectSetup.rootDir, "build.gradle").appendText(
+ """
+ android {
+ buildTypes {
+ staging {
+ initWith debug
+ applicationIdSuffix ".debugStaging"
+ }
+ }
+ }
+ """.trimIndent()
+ )
+
+ runGradleTasks(CLEAN_TASK, "compileStagingJavaWithJavac",).let { result ->
+ result.assertTaskOutcome(":compileStagingJavaWithJavac", TaskOutcome.SUCCESS)
+ result.assertTaskOutcome(":copyRoomSchemasStaging", TaskOutcome.SUCCESS)
+ }
+ val schemeFile = projectSetup.rootDir.resolve(
+ "schemas/staging/room.testapp.MyDatabase/1.json"
+ )
+ assertThat(schemeFile.exists()).isTrue()
+ }
+
+ private fun runGradleTasks(
+ vararg args: String,
+ projectDir: File = projectSetup.rootDir
+ ): BuildResult {
+ return GradleRunner.create()
+ .withProjectDir(projectDir)
+ .withPluginClasspath()
+ // workaround for b/231154556
+ .withArguments("-Dorg.gradle.jvmargs=-Xmx1g -XX:MaxMetaspaceSize=512m", *args)
+ .build()
+ }
+
+ private fun BuildResult.assertTaskOutcome(taskPath: String, outcome: TaskOutcome) {
+ assertThat(this.task(taskPath)!!.outcome).isEqualTo(outcome)
+ }
+
+ private fun searchAndReplace(file: File, search: String, replace: String) {
+ file.writeText(file.readText().replace(search, replace))
+ }
+
+ enum class ProcessingBackend(
+ val isForKotlin: Boolean
+ ) {
+ JAVAC(false),
+ KAPT(true),
+ KSP(true)
+ }
+
+ companion object {
+ private const val CLEAN_TASK = ":clean"
+ private const val COMPILE_TASK = ":compileDebugJavaWithJavac"
+ private const val COPY_TASK = ":copyRoomSchemasDebug"
+ }
+}
\ No newline at end of file
diff --git a/room/room-gradle-plugin/src/test/test-data/flavored-project/src/flavorOne/java/room/testapp/MyEntity.java b/room/room-gradle-plugin/src/test/test-data/flavored-project/src/flavorOne/java/room/testapp/MyEntity.java
new file mode 100644
index 0000000..8959321
--- /dev/null
+++ b/room/room-gradle-plugin/src/test/test-data/flavored-project/src/flavorOne/java/room/testapp/MyEntity.java
@@ -0,0 +1,26 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package room.testapp;
+
+import androidx.room.Entity;
+import androidx.room.PrimaryKey;
+
+@Entity
+public class MyEntity {
+ @PrimaryKey
+ public long id;
+}
diff --git a/room/room-gradle-plugin/src/test/test-data/flavored-project/src/flavorTwo/java/room/testapp/MyEntity.java b/room/room-gradle-plugin/src/test/test-data/flavored-project/src/flavorTwo/java/room/testapp/MyEntity.java
new file mode 100644
index 0000000..ea059e9
--- /dev/null
+++ b/room/room-gradle-plugin/src/test/test-data/flavored-project/src/flavorTwo/java/room/testapp/MyEntity.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package room.testapp;
+
+import androidx.room.Entity;
+import androidx.room.PrimaryKey;
+
+@Entity
+public class MyEntity {
+ @PrimaryKey
+ public long id;
+
+ public boolean flavorTwoColumn;
+}
diff --git a/room/room-gradle-plugin/src/test/test-data/flavored-project/src/main/AndroidManifest.xml b/room/room-gradle-plugin/src/test/test-data/flavored-project/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..1e3e702
--- /dev/null
+++ b/room/room-gradle-plugin/src/test/test-data/flavored-project/src/main/AndroidManifest.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright 2023 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+<manifest/>
\ No newline at end of file
diff --git a/room/room-gradle-plugin/src/test/test-data/flavored-project/src/main/java/room/testapp/MyDao.java b/room/room-gradle-plugin/src/test/test-data/flavored-project/src/main/java/room/testapp/MyDao.java
new file mode 100644
index 0000000..727ffbc
--- /dev/null
+++ b/room/room-gradle-plugin/src/test/test-data/flavored-project/src/main/java/room/testapp/MyDao.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package room.testapp;
+
+import androidx.room.Dao;
+import androidx.room.Query;
+
+import java.util.List;
+
+@Dao
+public interface MyDao {
+ @Query("SELECT * FROM MyEntity")
+ List<MyEntity> getAll();
+}
diff --git a/room/room-gradle-plugin/src/test/test-data/java/MyDatabase.java b/room/room-gradle-plugin/src/test/test-data/java/MyDatabase.java
new file mode 100644
index 0000000..29851d4
--- /dev/null
+++ b/room/room-gradle-plugin/src/test/test-data/java/MyDatabase.java
@@ -0,0 +1,25 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package room.testapp;
+
+import androidx.room.Database;
+import androidx.room.RoomDatabase;
+
+@Database(entities = { MyEntity.class }, version = 1)
+public abstract class MyDatabase extends RoomDatabase {
+ public abstract MyDao getDao();
+}
diff --git a/room/room-gradle-plugin/src/test/test-data/kotlin/MyDatabase.kt b/room/room-gradle-plugin/src/test/test-data/kotlin/MyDatabase.kt
new file mode 100644
index 0000000..8b03a9f
--- /dev/null
+++ b/room/room-gradle-plugin/src/test/test-data/kotlin/MyDatabase.kt
@@ -0,0 +1,25 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package room.testapp
+
+import androidx.room.Database
+import androidx.room.RoomDatabase
+
+@Database(entities = [MyEntity::class], version = 1)
+abstract class MyDatabase : RoomDatabase() {
+ abstract fun getDao(): MyDao
+}
\ No newline at end of file
diff --git a/room/room-gradle-plugin/src/test/test-data/simple-project/src/main/AndroidManifest.xml b/room/room-gradle-plugin/src/test/test-data/simple-project/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..1e3e702
--- /dev/null
+++ b/room/room-gradle-plugin/src/test/test-data/simple-project/src/main/AndroidManifest.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright 2023 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+<manifest/>
\ No newline at end of file
diff --git a/room/room-gradle-plugin/src/test/test-data/simple-project/src/main/java/room/testapp/MyDao.java b/room/room-gradle-plugin/src/test/test-data/simple-project/src/main/java/room/testapp/MyDao.java
new file mode 100644
index 0000000..727ffbc
--- /dev/null
+++ b/room/room-gradle-plugin/src/test/test-data/simple-project/src/main/java/room/testapp/MyDao.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package room.testapp;
+
+import androidx.room.Dao;
+import androidx.room.Query;
+
+import java.util.List;
+
+@Dao
+public interface MyDao {
+ @Query("SELECT * FROM MyEntity")
+ List<MyEntity> getAll();
+}
diff --git a/room/room-gradle-plugin/src/test/test-data/simple-project/src/main/java/room/testapp/MyEntity.java b/room/room-gradle-plugin/src/test/test-data/simple-project/src/main/java/room/testapp/MyEntity.java
new file mode 100644
index 0000000..3e4c1c0
--- /dev/null
+++ b/room/room-gradle-plugin/src/test/test-data/simple-project/src/main/java/room/testapp/MyEntity.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package room.testapp;
+
+import androidx.room.Entity;
+import androidx.room.PrimaryKey;
+
+@Entity
+public class MyEntity {
+ @PrimaryKey
+ public long id;
+
+ // Insert-change
+}
diff --git a/settings.gradle b/settings.gradle
index 6dc200a..aa1e705 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -1,6 +1,6 @@
-import groovy.transform.Field
import androidx.build.gradle.gcpbuildcache.GcpBuildCache
import androidx.build.gradle.gcpbuildcache.GcpBuildCacheServiceFactory
+import groovy.transform.Field
import java.util.regex.Matcher
import java.util.regex.Pattern
@@ -918,6 +918,7 @@
includeProject(":room:room-compiler-processing", [BuildType.MAIN, BuildType.COMPOSE, BuildType.FLAN])
includeProject(":room:room-compiler-processing-testing", [BuildType.MAIN, BuildType.COMPOSE, BuildType.FLAN])
includeProject(":room:room-guava", [BuildType.MAIN])
+includeProject(":room:room-gradle-plugin", [BuildType.MAIN])
includeProject(":room:room-ktx", [BuildType.MAIN, BuildType.COMPOSE])
includeProject(":room:room-migration", [BuildType.MAIN, BuildType.COMPOSE])
includeProject(":room:room-paging", [BuildType.MAIN, BuildType.COMPOSE])
diff --git a/testutils/testutils-gradle-plugin/src/main/java/androidx/testutils/gradle/ProjectSetupRule.kt b/testutils/testutils-gradle-plugin/src/main/java/androidx/testutils/gradle/ProjectSetupRule.kt
index 7bd12a1c..0724f1f 100644
--- a/testutils/testutils-gradle-plugin/src/main/java/androidx/testutils/gradle/ProjectSetupRule.kt
+++ b/testutils/testutils-gradle-plugin/src/main/java/androidx/testutils/gradle/ProjectSetupRule.kt
@@ -16,12 +16,15 @@
package androidx.testutils.gradle
+import java.io.File
+import java.util.Properties
+import javax.xml.parsers.DocumentBuilderFactory
+import javax.xml.xpath.XPathConstants
+import javax.xml.xpath.XPathFactory
import org.junit.rules.ExternalResource
import org.junit.rules.TemporaryFolder
import org.junit.runner.Description
import org.junit.runners.model.Statement
-import java.io.File
-import java.util.Properties
/**
* Test rule that helps to setup android project in tests that run gradle.
@@ -130,6 +133,42 @@
}
}
+ /**
+ * Gets the latest version of a published library.
+ *
+ * Note that the library must have been locally published to locate its latest version, this
+ * can be done in test by adding :publish as a test dependency, for example:
+ * ```
+ * tasks.findByPath("test")
+ * .dependsOn(tasks.findByPath(":room:room-compiler:publish")
+ * ```
+ *
+ * @param path - The library m2 path e.g. "androidx/room/room-compiler"
+ */
+ fun getLibraryLatestVersionInLocalRepo(path: String): String {
+ val metadataFile = File(props.tipOfTreeMavenRepoPath)
+ .resolve(path)
+ .resolve("maven-metadata.xml")
+ check(metadataFile.exists()) {
+ "Cannot find room metadata file in ${metadataFile.absolutePath}"
+ }
+ check(metadataFile.isFile) {
+ "Metadata file should be a file but it is not."
+ }
+ val xmlDoc = DocumentBuilderFactory.newInstance().newDocumentBuilder()
+ .parse(metadataFile)
+ val latestVersionNode = XPathFactory.newInstance().newXPath()
+ .compile("/metadata/versioning/latest").evaluate(
+ xmlDoc, XPathConstants.STRING
+ )
+ check(latestVersionNode is String) {
+ """Unexpected node for latest version:
+ $latestVersionNode / ${latestVersionNode::class.java}
+ """.trimIndent()
+ }
+ return latestVersionNode
+ }
+
private fun copyLocalProperties() {
var foundSdk = false