Skip to content
Closed
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
1 change: 1 addition & 0 deletions compose/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ kotlin {
publishLibraryVariants("release", "debug")
}

jvm()
iosX64()
iosArm64()
iosSimulatorArm64()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.powersync.compose

import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import com.powersync.DatabaseDriverFactory

@Composable
public actual fun rememberDatabaseDriverFactory(): DatabaseDriverFactory {
return remember {
DatabaseDriverFactory()
}
}
1 change: 1 addition & 0 deletions connectors/supabase/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ kotlin {
}
}

jvm()
iosX64()
iosArm64()
iosSimulatorArm64()
Expand Down
40 changes: 36 additions & 4 deletions core/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import com.powersync.plugins.sonatype.setupGithubRepository
import de.undercouch.gradle.tasks.download.Download
import org.apache.tools.ant.taskdefs.condition.Os
import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget
import com.powersync.plugins.sonatype.setupGithubRepository

plugins {
alias(libs.plugins.kotlinMultiplatform)
Expand All @@ -14,8 +15,15 @@ plugins {
val sqliteVersion = "3450000"
val sqliteReleaseYear = "2024"

val sqliteSrcFolder =
project.layout.buildDirectory.dir("interop/sqlite").get()
val osName = when {
Os.isFamily(Os.FAMILY_WINDOWS) -> "Windows"
Os.isFamily(Os.FAMILY_MAC) -> "macOS"
Os.isFamily(Os.FAMILY_UNIX) -> "Linux"
else -> "Unknown"
}

val sqliteSrcFolder = project.layout.buildDirectory.dir("interop/sqlite").get()
val jvmNativeBuildFolder = "${layout.buildDirectory.get().asFile}/native/powersync-sqlite"

val downloadSQLiteSources by tasks.registering(Download::class) {
val zipFileName = "sqlite-amalgamation-$sqliteVersion.zip"
Expand Down Expand Up @@ -59,6 +67,19 @@ val buildCInteropDef by tasks.registering {
outputs.files(defFile)
}

tasks.create<Exec>("buildNative") {
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Run build.sh to build the binary and save it to build/native/powersync-sqlite/${OS}/output

group = "build"

outputs.dir(jvmNativeBuildFolder)
workingDir = file(jvmNativeBuildFolder)

environment("TARGET", "$osName/cmake")
environment("SOURCE_PATH", "$projectDir/src/jvmMain/cpp")
environment("INTEROP_PATH", "$sqliteSrcFolder")

commandLine("$projectDir/src/jvmMain/cpp/build.sh")
}

kotlin {
androidTarget {
publishLibraryVariants("release", "debug")
Expand All @@ -67,6 +88,13 @@ kotlin {
iosX64()
iosArm64()
iosSimulatorArm64()
jvm {
val processResources = compilations["main"].processResourcesTaskName
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bundle the binary into the jar

(tasks[processResources] as ProcessResources).apply {
dependsOn("buildNative")
from("$jvmNativeBuildFolder/${osName}/output")
}
}

targets.withType<KotlinNativeTarget> {
compilations.getByName("main") {
Expand Down Expand Up @@ -113,6 +141,11 @@ kotlin {
implementation(libs.ktor.client.ios)
}

jvmMain.dependencies {
implementation(libs.sqldelight.driver.desktop)
implementation(libs.ktor.client.okhttp)
}

commonTest.dependencies {
implementation(libs.kotlin.test)
}
Expand Down Expand Up @@ -180,4 +213,3 @@ afterEvaluate {
}

setupGithubRepository()

27 changes: 27 additions & 0 deletions core/src/jvmMain/cpp/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
cmake_minimum_required(VERSION 3.18.1)

project(powersync-sqlite)

set(PACKAGE_NAME "powersync-sqlite")
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/../output)

find_package(JNI REQUIRED)

include_directories(${JNI_INCLUDE_DIRS})
include_directories(${SQLITE3_INTEROP_DIR})

add_library(sqlite3_static STATIC
"${SQLITE3_INTEROP_DIR}/sqlite3.c"
)

add_library(
${PACKAGE_NAME}
SHARED
sqlite_bindings.cpp
)

target_link_libraries(
${PACKAGE_NAME}
PRIVATE
sqlite3_static
)
12 changes: 12 additions & 0 deletions core/src/jvmMain/cpp/build.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
#!/bin/bash
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure whats the convention for the location of build files 🤔

set -e

[[ -z "$TARGET" ]] && echo "Please set the PLATFORM variable" && exit 1
[[ -z "$SOURCE_PATH" ]] && echo "Please set the SOURCE_PATH variable" && exit 1
[[ -z "$INTEROP_PATH" ]] && echo "Please set the INTEROP_PATH variable" && exit 1

mkdir -p "$TARGET"
cd "$TARGET"

cmake -DSQLITE3_INTEROP_DIR="$INTEROP_PATH" "$SOURCE_PATH"
cmake --build .
91 changes: 91 additions & 0 deletions core/src/jvmMain/cpp/sqlite_bindings.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
#include <jni.h>
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mentioned in the PR note, this is copied from androidMain, would be good to share the file

#include <inttypes.h>
#include <sqlite3.h>
#include <string>

typedef struct context {
JavaVM *javaVM;
jobject bindingsObj;
jclass bindingsClz;
} Context;
Context g_ctx;

extern "C" {

JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved) {
JNIEnv *env;
memset(&g_ctx, 0, sizeof(g_ctx));
g_ctx.javaVM = vm;

if (vm->GetEnv((void **) &env, JNI_VERSION_1_6) != JNI_OK) {
return JNI_ERR; // JNI version not supported.
}

return JNI_VERSION_1_6;
}

static void
update_hook_callback(void *pData, int opCode, char const *pDbName, char const *pTableName,
sqlite3_int64 iRow) {
// Get JNIEnv for the current thread
JNIEnv *env;
JavaVM *javaVM = g_ctx.javaVM;
javaVM->GetEnv((void **) &env, JNI_VERSION_1_6);

if (g_ctx.bindingsClz) {
jmethodID updateId = env->GetMethodID(
g_ctx.bindingsClz, "onTableUpdate", "(Ljava/lang/String;)V");

jstring tableString = env->NewStringUTF(std::string(pTableName).c_str());
env->CallVoidMethod(g_ctx.bindingsObj, updateId, tableString);
}
}

static jint
commit_hook(void *pool) {
// Get JNIEnv for the current thread
JNIEnv *env;
JavaVM *javaVM = g_ctx.javaVM;
javaVM->GetEnv((void **) &env, JNI_VERSION_1_6);

if (g_ctx.bindingsClz) {
jmethodID methodId = env->GetMethodID(
g_ctx.bindingsClz, "onTransactionCommit", "(Z)V");

env->CallVoidMethod(g_ctx.bindingsObj, methodId, JNI_TRUE);
}

return 0;
}

static void rollback_hook(void *pool) {
// Get JNIEnv for the current thread
JNIEnv *env;
JavaVM *javaVM = g_ctx.javaVM;
javaVM->GetEnv((void **) &env, JNI_VERSION_1_6);

if (g_ctx.bindingsClz) {
jmethodID methodId = env->GetMethodID(
g_ctx.bindingsClz, "onTransactionCommit", "(Z)V");

env->CallVoidMethod(g_ctx.bindingsObj, methodId, JNI_FALSE);
}
}

jint powersync_init(sqlite3 *db, char **pzErrMsg,
const sqlite3_api_routines *pApi) {

sqlite3_update_hook(db, update_hook_callback, NULL);
sqlite3_commit_hook(db, commit_hook, NULL);
sqlite3_rollback_hook(db, rollback_hook, NULL);

return SQLITE_OK;
}

JNIEXPORT void JNICALL
Java_com_powersync_DatabaseDriverFactory_setupSqliteBinding(JNIEnv *env, jobject thiz) {
jclass clz = env->GetObjectClass(thiz);
g_ctx.bindingsClz = (jclass) env->NewGlobalRef(clz);
g_ctx.bindingsObj = env->NewGlobalRef(thiz);
}
}
6 changes: 6 additions & 0 deletions core/src/jvmMain/kotlin/BuidConfig.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
public actual object BuildConfig {
public actual val isDebug: Boolean
// TODO: need to determine a good way to set this on JVM presuming we don't want to bundle BuildKonfig in the
// library.
get() = true
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Didn't want to add the dependency on BuildKonfig to the library, will explore if there is an alternative.

}
50 changes: 50 additions & 0 deletions core/src/jvmMain/kotlin/com/powersync/DatabaseDriverFactory.jvm.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package com.powersync

import app.cash.sqldelight.async.coroutines.synchronous
import app.cash.sqldelight.driver.jdbc.sqlite.JdbcSqliteDriver
import com.powersync.db.internal.InternalSchema
import java.util.Properties
import kotlinx.coroutines.CoroutineScope

@Suppress("EXPECT_ACTUAL_CLASSIFIERS_ARE_IN_BETA_WARNING")
public actual class DatabaseDriverFactory {
private var driver: PsSqlDriver? = null
private external fun setupSqliteBinding()

@Suppress("unused")
private fun onTableUpdate(tableName: String) {
driver?.updateTable(tableName)
}

@Suppress("unused")
private fun onTransactionCommit(success: Boolean) {
driver?.also { driver ->
if (success) {
driver.fireTableUpdates()
} else {
driver.clearTableUpdates()
}
}
}

public actual fun createDriver(
scope: CoroutineScope,
dbFilename: String
): PsSqlDriver {
val schema = InternalSchema.synchronous()
this.driver = PsSqlDriver(
scope = scope,
driver = JdbcSqliteDriver("jdbc:sqlite:$dbFilename", Properties(), schema)
Copy link
Author

@henwoods henwoods Jul 25, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is the cause of the crash, not loading sqlite driver with extension support so the powersync_rs_version isn't available

)
setupSqliteBinding()
return this.driver as PsSqlDriver
}

public companion object {
init {
// There is presumably a better way to load the library from the jar.
@Suppress("UnsafeDynamicallyLoadedCode")
System.load(OSXLibraryLoader().loadLibraryFromResources())
}
}
}
38 changes: 38 additions & 0 deletions core/src/jvmMain/kotlin/com/powersync/LibraryLoader.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package com.powersync

import com.powersync.LibraryLoader.Companion.SQLITE_BINARY_FILENAME
import java.io.File
import java.io.InputStream

private interface LibraryLoader {
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AI recommended this approach so I'm sure there is a better way 😂

companion object {
const val SQLITE_BINARY_FILENAME: String = "libpowersync-sqlite"
}

fun loadLibraryFromResources(): String

fun createTempFile(prefix: String, suffix: String): File {
val dir = System.getProperty("java.io.tmpdir")
return File.createTempFile(prefix, suffix, File(dir))
}
}

// TODO: Need to create and test implementations of this for windows/linux, or even better implement a cleaner
// of extracting the shared library.
public class OSXLibraryLoader : LibraryLoader {
override fun loadLibraryFromResources(): String {
val path = "/$SQLITE_BINARY_FILENAME.dylib"
val tempFile = createTempFile(SQLITE_BINARY_FILENAME, ".dylib")
tempFile.deleteOnExit()

val inputStream: InputStream = LibraryLoader::class.java.getResourceAsStream(path)
?: throw IllegalArgumentException("File $path not found in resources")
inputStream.use { input ->
tempFile.outputStream().use { output ->
input.copyTo(output)
}
}

return tempFile.absolutePath
}
}
2 changes: 2 additions & 0 deletions demos/hello-powersync/composeApp/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ plugins {
kotlin {
androidTarget()

jvm()

// iosX64() // uncomment to enable iOS x64
iosArm64()
iosSimulatorArm64()
Expand Down
28 changes: 28 additions & 0 deletions demos/hello-powersync/desktop/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import org.jetbrains.compose.desktop.application.dsl.TargetFormat

plugins {
// Gradle complains when I attempt to re-use the plugin definition from the project toml. Something to do
// with the way the sonatype module is set up I presume. Need to figure this out before merge
id("org.jetbrains.kotlin.jvm")
alias(projectLibs.plugins.jetbrainsCompose)
}

dependencies {
implementation(project(":composeApp"))
implementation(compose.desktop.currentOs)
}

group = "com.powersync"
version = "1.0.0"

compose.desktop {
application {
mainClass = "com.powersync.demos.MainKt"

nativeDistributions {
targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb)
packageName = "PowerSync Demo"
packageVersion = project.version as String
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.powersync.demos

import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Window
import androidx.compose.ui.window.application
import androidx.compose.ui.window.rememberWindowState

fun main() = application {
val windowState = rememberWindowState(size = DpSize(1200.dp, 900.dp))

Window(state = windowState, onCloseRequest = ::exitApplication, title = "PowerSync Demo") {
App()
}
}
Loading