Use a custom sqlite-jdbc native library loader.
* Update sqlite-jdbc to 3.34.0
* Use a custom loader in Room instead of SQLiteJDBCLoader since it
workarounds current issues in the loading strategy,
specifically: https://github.com/xerial/sqlite-jdbc/pull/578.
Fixes: 177673291
Test: ./gradlew bOS
Change-Id: Idc597b734bc4d7bc03ffe938d2306b31193ada27
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index b695822..adecbc3 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -131,7 +131,7 @@
shadow = { module = "com.github.jengelman.gradle.plugins:shadow", version = "6.1.0" }
sqldelightAndroid = { module = "com.squareup.sqldelight:android-driver", version.ref = "sqldelight" }
sqldelightCoroutinesExt = { module = "com.squareup.sqldelight:coroutines-extensions", version.ref = "sqldelight" }
-sqliteJdbc = { module = "org.xerial:sqlite-jdbc", version = "3.25.2" }
+sqliteJdbc = { module = "org.xerial:sqlite-jdbc", version = "3.34.0" }
testCore = { module = "androidx.test:core", version.ref = "androidxTest" }
testExtJunit = { module = "androidx.test.ext:junit", version.ref = "androidxTestExt" }
testExtJunitKtx = { module = "androidx.test.ext:junit-ktx", version.ref = "androidxTestExt" }
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/verifier/DatabaseVerifier.kt b/room/room-compiler/src/main/kotlin/androidx/room/verifier/DatabaseVerifier.kt
index d7fd403..b435de1 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/verifier/DatabaseVerifier.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/verifier/DatabaseVerifier.kt
@@ -26,7 +26,6 @@
import androidx.room.vo.Warning
import columnInfo
import org.sqlite.JDBC
-import org.sqlite.SQLiteJDBCLoader
import java.io.File
import java.sql.Connection
import java.sql.SQLException
@@ -68,7 +67,7 @@
// multiple library versions, process isolation and multiple class loaders by using
// UUID named library files.
synchronized(System::class.java) {
- SQLiteJDBCLoader.initialize() // extract and loads native library
+ NativeSQLiteLoader.load() // extract and loads native library
JDBC.isValidURL(CONNECTION_URL) // call to register driver
}
}
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/verifier/NativeSQLiteLoader.kt b/room/room-compiler/src/main/kotlin/androidx/room/verifier/NativeSQLiteLoader.kt
new file mode 100644
index 0000000..677411c
--- /dev/null
+++ b/room/room-compiler/src/main/kotlin/androidx/room/verifier/NativeSQLiteLoader.kt
@@ -0,0 +1,158 @@
+/*
+ * Copyright 2021 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.verifier
+
+import org.sqlite.SQLiteJDBCLoader
+import org.sqlite.util.OSInfo
+import java.io.File
+import java.io.IOException
+import java.io.InputStream
+import java.util.UUID
+
+/**
+ * A custom sqlite-jdbc native library extractor and loader.
+ *
+ * This class is used instead of [SQLiteJDBCLoader.initialize] since it workarounds current issues
+ * in the loading strategy, specifically: https://github.com/xerial/sqlite-jdbc/pull/578.
+ */
+internal object NativeSQLiteLoader {
+
+ private var loaded = false
+
+ private val tempDir: File by lazy {
+ File(System.getProperty("org.sqlite.tmpdir", System.getProperty("java.io.tmpdir")))
+ }
+
+ private val version: String by lazy { SQLiteJDBCLoader.getVersion() }
+
+ @JvmStatic
+ fun load() = synchronized(loaded) {
+ if (loaded) return
+ try {
+ // Cleanup target temporary folder for a new extraction.
+ cleanupTempFolder()
+ // Extract and load native library.
+ loadNativeLibrary()
+ // Reflect into original loader and mark library as extracted.
+ SQLiteJDBCLoader::class.java.getDeclaredField("extracted")
+ .apply { trySetAccessible() }
+ .set(null, true)
+ } catch (ex: Exception) {
+ // Fallback to main library if our attempt failed, do print error juuust in case, so if
+ // there is an error with our approach we get to know, instead of fully swallowing it.
+ RuntimeException("Failed to load native SQLite library, will try again though.", ex)
+ .printStackTrace()
+ SQLiteJDBCLoader.initialize()
+ }
+ loaded = true
+ }
+
+ private fun cleanupTempFolder() {
+ tempDir.listFiles { file ->
+ file.name.startsWith("sqlite-$version") && !file.name.endsWith(".lck")
+ }?.forEach { libFile ->
+ val lckFile = File(libFile.absolutePath + ".lck")
+ if (!lckFile.exists()) {
+ libFile.delete()
+ }
+ }
+ }
+
+ // Load the OS-dependent library from the Jar file.
+ private fun loadNativeLibrary() {
+ val packagePath =
+ SQLiteJDBCLoader::class.java.getPackage().name.replace(".", "/")
+ val nativeLibraryPath =
+ "/$packagePath/native/${OSInfo.getNativeLibFolderPathForCurrentOS()}"
+ val nativeLibraryName = let {
+ val libName = System.mapLibraryName("sqlitejdbc")
+ .apply { replace("dylib", "jnilib") }
+ if (hasResource("$nativeLibraryPath/$libName")) {
+ return@let libName
+ }
+ if (OSInfo.getOSName() == "Mac") {
+ // Fix for openjdk7 for Mac
+ val altLibName = "libsqlitejdbc.jnilib"
+ if (hasResource("$nativeLibraryPath/$altLibName")) {
+ return@let altLibName
+ }
+ }
+ error(
+ "No native library is found for os.name=${OSInfo.getOSName()} and " +
+ "os.arch=${OSInfo.getArchName()}. path=$nativeLibraryPath"
+ )
+ }
+
+ val extractedNativeLibraryFile = try {
+ extractNativeLibrary(nativeLibraryPath, nativeLibraryName, tempDir.absolutePath)
+ } catch (ex: IOException) {
+ throw RuntimeException("Couldn't extract native SQLite library.", ex)
+ }
+ try {
+ @Suppress("UnsafeDynamicallyLoadedCode") // Loading an from an absolute path.
+ System.load(extractedNativeLibraryFile.absolutePath)
+ } catch (ex: UnsatisfiedLinkError) {
+ throw RuntimeException("Couldn't load native SQLite library.", ex)
+ }
+ }
+
+ private fun extractNativeLibrary(
+ libraryPath: String,
+ libraryName: String,
+ targetDirPath: String
+ ): File {
+ val libraryFilePath = "$libraryPath/$libraryName"
+ // Include arch name in temporary filename in order to avoid conflicts when multiple JVMs
+ // with different architectures are running.
+ val outputLibraryFile = File(
+ targetDirPath,
+ "sqlite-$version-${UUID.randomUUID()}-$libraryName"
+ ).apply { deleteOnExit() }
+ val outputLibraryLckFile = File(
+ targetDirPath,
+ "${outputLibraryFile.name}.lck"
+ ).apply { deleteOnExit() }
+ if (!outputLibraryLckFile.exists()) {
+ outputLibraryLckFile.outputStream().close()
+ }
+ getResourceAsStream(libraryFilePath).use { inputStream ->
+ outputLibraryFile.outputStream().use { outputStream ->
+ inputStream.copyTo(outputStream)
+ }
+ }
+ // Set executable flag (x) to enable loading the library.
+ outputLibraryFile.setReadable(true)
+ outputLibraryFile.setExecutable(true)
+ return outputLibraryFile
+ }
+
+ private fun hasResource(path: String) = SQLiteJDBCLoader::class.java.getResource(path) != null
+
+ // Replacement of java.lang.Class#getResourceAsStream(String) to disable sharing the resource
+ // stream in multiple class loaders and specifically to avoid
+ // https://bugs.openjdk.java.net/browse/JDK-8205976
+ private fun getResourceAsStream(name: String): InputStream {
+ // Remove leading '/' since all our resource paths include a leading directory
+ // See: https://github.com/openjdk/jdk/blob/jdk-11+0/src/java.base/share/classes/java/lang/Class.java#L2573
+ val resolvedName = name.drop(1)
+ val url = SQLiteJDBCLoader::class.java.classLoader.getResource(resolvedName)
+ ?: throw IOException("Resource '$resolvedName' could not be found.")
+ return url.openConnection().apply {
+ defaultUseCaches = false
+ }.getInputStream()
+ }
+}
\ No newline at end of file
diff --git a/room/room-compiler/src/test/kotlin/androidx/room/verifier/NativeSQLiteLoaderTest.kt b/room/room-compiler/src/test/kotlin/androidx/room/verifier/NativeSQLiteLoaderTest.kt
new file mode 100644
index 0000000..8cf547b
--- /dev/null
+++ b/room/room-compiler/src/test/kotlin/androidx/room/verifier/NativeSQLiteLoaderTest.kt
@@ -0,0 +1,132 @@
+/*
+ * Copyright 2021 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.verifier
+
+import com.google.common.truth.Truth.assertThat
+import org.junit.Assert.fail
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import java.io.File
+import java.net.URL
+import java.net.URLClassLoader
+import java.util.concurrent.Executors
+import java.util.concurrent.TimeUnit
+import java.util.concurrent.atomic.AtomicInteger
+import java.util.jar.JarEntry
+import java.util.jar.JarOutputStream
+
+@RunWith(JUnit4::class)
+class NativeSQLiteLoaderTest {
+
+ @Test
+ fun multipleClassLoader() {
+ // Get current classpath
+ val stringUrls = System.getProperty("java.class.path")!!
+ .split(System.getProperty("path.separator")!!).toTypedArray()
+ // Find classes under test.
+ val targetDirName = "room-compiler/build/classes/kotlin/main"
+ val classesDirPath = stringUrls.first { it.contains(targetDirName) }
+ // Create a JAR file out the classes and resources
+ val jarFile = File.createTempFile("jar-for-test-", ".jar")
+ createJar(classesDirPath, jarFile)
+ val jarUrl = URL("file://${jarFile.absolutePath}")
+ // Find Kotlin stdlibs (will need them to load class under test)
+ val kotlinStdbLibUrls = stringUrls
+ .filter { it.contains("kotlin-stdlib") && it.endsWith(".jar") }
+ .map { URL("file://$it") }
+ // Also find sqlite-jdbc since it is a hard dep of NativeSQLiteLoader
+ val sqliteJdbcJarUrl = stringUrls
+ .filter { it.contains("sqlite-jdbc") && it.endsWith(".jar") }
+ .map { URL("file://$it") }
+ // Spawn a few threads and have them all in parallel load the native lib
+ val completedThreads = AtomicInteger(0)
+ val numOfThreads = 8
+ val pool = Executors.newFixedThreadPool(numOfThreads)
+ val loadedClasses = arrayOfNulls<Class<*>>(numOfThreads)
+ for (i in 1..numOfThreads) {
+ pool.execute {
+ try {
+ Thread.sleep((i * 10).toLong())
+ // Create an isolated class loader, it should load *different* instances
+ // of NativeSQLiteLoader.class
+ val classLoader = URLClassLoader(
+ (kotlinStdbLibUrls + sqliteJdbcJarUrl + jarUrl).toTypedArray(),
+ ClassLoader.getSystemClassLoader().parent
+ )
+ val clazz =
+ classLoader.loadClass("androidx.room.verifier.NativeSQLiteLoader")
+ clazz.getDeclaredMethod("load").invoke(null)
+ classLoader.close()
+ loadedClasses[i - 1] = clazz
+ } catch (e: Throwable) {
+ e.printStackTrace()
+ fail(e.message)
+ }
+ completedThreads.incrementAndGet()
+ }
+ }
+ // Verify all threads completed
+ pool.shutdown()
+ pool.awaitTermination(3, TimeUnit.SECONDS)
+ assertThat(completedThreads.get()).isEqualTo(numOfThreads)
+ // Verify all loaded classes are different from each other
+ loadedClasses.forEachIndexed { i, clazz1 ->
+ loadedClasses.forEachIndexed { j, clazz2 ->
+ if (i == j) {
+ assertThat(clazz1).isEqualTo(clazz2)
+ } else {
+ assertThat(clazz1).isNotEqualTo(clazz2)
+ }
+ }
+ }
+ }
+
+ private fun createJar(inputDir: String, outputFile: File) {
+ JarOutputStream(outputFile.outputStream()).use {
+ addJarEntry(File(inputDir), inputDir, it)
+ }
+ }
+
+ private fun addJarEntry(source: File, changeDir: String, target: JarOutputStream) {
+ if (source.isDirectory) {
+ var name = source.path.replace("\\", "/")
+ if (name.isNotEmpty()) {
+ if (!name.endsWith("/")) {
+ name += "/"
+ }
+ val entry = JarEntry(name.substring(changeDir.length + 1))
+ entry.time = source.lastModified()
+ target.putNextEntry(entry)
+ target.closeEntry()
+ }
+ source.listFiles()!!.forEach { nestedFile ->
+ addJarEntry(nestedFile, changeDir, target)
+ }
+ } else if (source.isFile) {
+ val entry = JarEntry(
+ source.path.replace("\\", "/").substring(changeDir.length + 1)
+ )
+ entry.time = source.lastModified()
+ target.putNextEntry(entry)
+ source.inputStream().use { inputStream ->
+ inputStream.copyTo(target)
+ }
+ target.closeEntry()
+ }
+ }
+}
\ No newline at end of file