From 20f85959612716ea97ac7bb6a446ed79c14848ca Mon Sep 17 00:00:00 2001 From: DominicGBauer Date: Thu, 16 Jan 2025 14:59:58 +0200 Subject: [PATCH] feat: add macos to xcframework --- PowerSyncKotlin/build.gradle.kts | 11 +- core/build.gradle.kts | 269 ++++++++++-------- core/src/macosMain/kotlin/BuildConfig.kt | 8 + .../powersync/DatabaseDriverFactory.macos.kt | 132 +++++++++ persistence/build.gradle.kts | 4 +- 5 files changed, 297 insertions(+), 127 deletions(-) create mode 100644 core/src/macosMain/kotlin/BuildConfig.kt create mode 100644 core/src/macosMain/kotlin/com/powersync/DatabaseDriverFactory.macos.kt diff --git a/PowerSyncKotlin/build.gradle.kts b/PowerSyncKotlin/build.gradle.kts index 2e8d2589..a209c766 100644 --- a/PowerSyncKotlin/build.gradle.kts +++ b/PowerSyncKotlin/build.gradle.kts @@ -1,7 +1,6 @@ import co.touchlab.faktory.artifactmanager.ArtifactManager import co.touchlab.faktory.capitalized import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget -import org.jetbrains.kotlin.ir.backend.js.compile import java.net.URL import java.security.MessageDigest @@ -19,6 +18,8 @@ kotlin { iosX64(), iosArm64(), iosSimulatorArm64(), + macosArm64(), + macosX64(), ).forEach { it.binaries.framework { export(project(":core")) @@ -63,7 +64,7 @@ class SonatypePortalPublishArtifactManager( val project: Project, private val publicationName: String = "KMMBridgeFramework", artifactSuffix: String = "kmmbridge", - private val repositoryName: String? + private val repositoryName: String?, ) : ArtifactManager { private val group: String = project.group.toString().replace(".", "/") private val kmmbridgeArtifactId = @@ -73,19 +74,19 @@ class SonatypePortalPublishArtifactManager( // This is the URL that will be added to Package.swift in Github package so that // KMMBridge is downloaded when a user includes the package in XCode - private val MAVEN_CENTRAL_PACKAGE_ZIP_URL = "https://repo1.maven.org/maven2/com/powersync/${zipName}/${LIBRARY_VERSION}/${zipName}-${LIBRARY_VERSION}.zip" + private val MAVEN_CENTRAL_PACKAGE_ZIP_URL = "https://repo1.maven.org/maven2/com/powersync/$zipName/${LIBRARY_VERSION}/$zipName-${LIBRARY_VERSION}.zip" override fun deployArtifact( project: Project, zipFilePath: File, - version: String + version: String, ): String = MAVEN_CENTRAL_PACKAGE_ZIP_URL override fun configure( project: Project, version: String, uploadTask: TaskProvider, - kmmPublishTask: TaskProvider + kmmPublishTask: TaskProvider, ) { project.extensions.getByType().publications.create( publicationName, diff --git a/core/build.gradle.kts b/core/build.gradle.kts index 44c29c7e..582ca563 100644 --- a/core/build.gradle.kts +++ b/core/build.gradle.kts @@ -82,6 +82,8 @@ kotlin { iosX64() iosArm64() iosSimulatorArm64() + macosX64() + macosArm64() targets.withType { compilations.named("main") { @@ -134,7 +136,7 @@ kotlin { implementation(libs.sqlite.jdbc) } - iosMain.dependencies { + appleMain.dependencies { implementation(libs.ktor.client.ios) } @@ -203,137 +205,162 @@ if (binariesAreProvided && crossArch) { error("powersync.binaries.provided and powersync.binaries.cross-arch must not be both defined.") } -val getBinaries = if (binariesAreProvided) { - // Binaries for all OS must be provided (manually or by the CI) in binaries/desktop - - val verifyPowersyncBinaries = tasks.register("verifyPowersyncBinaries") { - val directory = projectDir.resolve("binaries/desktop") - val binaries = listOf( - directory.resolve("libpowersync-sqlite_aarch64.so"), - directory.resolve("libpowersync-sqlite_x64.so"), - directory.resolve("libpowersync-sqlite_aarch64.dylib"), - directory.resolve("libpowersync-sqlite_x64.dylib"), - directory.resolve("powersync-sqlite_x64.dll"), - ) - doLast { - binaries.forEach { - if (!it.exists()) error("File $it does not exist") - if (!it.isFile) error("File $it is not a regular file") +val getBinaries = + if (binariesAreProvided) { + // Binaries for all OS must be provided (manually or by the CI) in binaries/desktop + + val verifyPowersyncBinaries = + tasks.register("verifyPowersyncBinaries") { + val directory = projectDir.resolve("binaries/desktop") + val binaries = + listOf( + directory.resolve("libpowersync-sqlite_aarch64.so"), + directory.resolve("libpowersync-sqlite_x64.so"), + directory.resolve("libpowersync-sqlite_aarch64.dylib"), + directory.resolve("libpowersync-sqlite_x64.dylib"), + directory.resolve("powersync-sqlite_x64.dll"), + ) + doLast { + binaries.forEach { + if (!it.exists()) error("File $it does not exist") + if (!it.isFile) error("File $it is not a regular file") + } + } + outputs.files(*binaries.toTypedArray()) } + verifyPowersyncBinaries + } else { + // Building locally for the current OS + + val localProperties = Properties() + val localPropertiesFile = rootProject.file("local.properties") + if (localPropertiesFile.exists()) { + localPropertiesFile.inputStream().use { localProperties.load(it) } } - outputs.files(*binaries.toTypedArray()) - } - verifyPowersyncBinaries -} else { - // Building locally for the current OS - - val localProperties = Properties() - val localPropertiesFile = rootProject.file("local.properties") - if (localPropertiesFile.exists()) { - localPropertiesFile.inputStream().use { localProperties.load(it) } - } - val cmakeExecutable = localProperties.getProperty("cmake.path") ?: "cmake" - - fun registerCMakeTasks( - suffix: String, - vararg defines: String, - ): TaskProvider { - val cmakeConfigure = tasks.register("cmakeJvmConfigure${suffix.capitalize()}") { - dependsOn(unzipSQLiteSources) - group = "cmake" - workingDir = layout.buildDirectory.dir("cmake/$suffix").get().asFile - inputs.files( - "src/jvmMain/cpp", - "src/jvmNative/cpp", - sqliteSrcFolder, - ) - outputs.dir(workingDir) - executable = cmakeExecutable - args(listOf(file("src/jvmMain/cpp/CMakeLists.txt").absolutePath, "-DSUFFIX=$suffix", "-DCMAKE_BUILD_TYPE=Release") + defines.map { "-D$it" }) - doFirst { - workingDir.mkdirs() - } + val cmakeExecutable = localProperties.getProperty("cmake.path") ?: "cmake" + + fun registerCMakeTasks( + suffix: String, + vararg defines: String, + ): TaskProvider { + val cmakeConfigure = + tasks.register("cmakeJvmConfigure${suffix.capitalize()}") { + dependsOn(unzipSQLiteSources) + group = "cmake" + workingDir = + layout.buildDirectory + .dir("cmake/$suffix") + .get() + .asFile + inputs.files( + "src/jvmMain/cpp", + "src/jvmNative/cpp", + sqliteSrcFolder, + ) + outputs.dir(workingDir) + executable = cmakeExecutable + args( + listOf(file("src/jvmMain/cpp/CMakeLists.txt").absolutePath, "-DSUFFIX=$suffix", "-DCMAKE_BUILD_TYPE=Release") + + defines.map { "-D$it" }, + ) + doFirst { + workingDir.mkdirs() + } + } + + val cmakeBuild = + tasks.register("cmakeJvmBuild${suffix.capitalize()}") { + dependsOn(cmakeConfigure) + group = "cmake" + workingDir = + layout.buildDirectory + .dir("cmake/$suffix") + .get() + .asFile + inputs.files( + "src/jvmMain/cpp", + "src/jvmNative/cpp", + sqliteSrcFolder, + workingDir, + ) + outputs.dir(workingDir.resolve(if (os.isWindows) "output/Release" else "output")) + executable = cmakeExecutable + args("--build", ".", "--config", "Release") + } + + return cmakeBuild } - val cmakeBuild = tasks.register("cmakeJvmBuild${suffix.capitalize()}") { - dependsOn(cmakeConfigure) - group = "cmake" - workingDir = layout.buildDirectory.dir("cmake/$suffix").get().asFile - inputs.files( - "src/jvmMain/cpp", - "src/jvmNative/cpp", - sqliteSrcFolder, - workingDir, - ) - outputs.dir(workingDir.resolve(if (os.isWindows) "output/Release" else "output")) - executable = cmakeExecutable - args("--build", ".", "--config", "Release") - } + val (aarch64, x64) = + when { + os.isMacOsX -> { + val aarch64 = registerCMakeTasks("aarch64", "CMAKE_OSX_ARCHITECTURES=arm64") + val x64 = registerCMakeTasks("x64", "CMAKE_OSX_ARCHITECTURES=x86_64") + aarch64 to x64 + } + os.isLinux -> { + val aarch64 = + registerCMakeTasks("aarch64", "CMAKE_C_COMPILER=aarch64-linux-gnu-gcc", "CMAKE_CXX_COMPILER=aarch64-linux-gnu-g++") + val x64 = registerCMakeTasks("x64", "CMAKE_C_COMPILER=x86_64-linux-gnu-gcc", "CMAKE_CXX_COMPILER=x86_64-linux-gnu-g++") + aarch64 to x64 + } + os.isWindows -> { + val x64 = registerCMakeTasks("x64") + null to x64 + } + else -> error("Unknown operating system: $os") + } - return cmakeBuild - } + val arch = System.getProperty("os.arch") + val cmakeJvmBuilds = + when { + crossArch -> listOfNotNull(aarch64, x64) + arch == "aarch64" -> listOfNotNull(aarch64) + arch == "amd64" || arch == "x86_64" -> listOfNotNull(x64) + else -> error("Unsupported architecture: $arch") + } - val (aarch64, x64) = when { - os.isMacOsX -> { - val aarch64 = registerCMakeTasks("aarch64", "CMAKE_OSX_ARCHITECTURES=arm64") - val x64 = registerCMakeTasks("x64", "CMAKE_OSX_ARCHITECTURES=x86_64") - aarch64 to x64 - } - os.isLinux -> { - val aarch64 = registerCMakeTasks("aarch64", "CMAKE_C_COMPILER=aarch64-linux-gnu-gcc", "CMAKE_CXX_COMPILER=aarch64-linux-gnu-g++") - val x64 = registerCMakeTasks("x64", "CMAKE_C_COMPILER=x86_64-linux-gnu-gcc", "CMAKE_CXX_COMPILER=x86_64-linux-gnu-g++") - aarch64 to x64 - } - os.isWindows -> { - val x64 = registerCMakeTasks("x64") - null to x64 + tasks.register("cmakeJvmBuild") { + dependsOn(cmakeJvmBuilds) + group = "cmake" + from(cmakeJvmBuilds) + into(binariesFolder.map { it.dir("sqlite") }) } - else -> error("Unknown operating system: $os") - } - - val arch = System.getProperty("os.arch") - val cmakeJvmBuilds = when { - crossArch -> listOfNotNull(aarch64, x64) - arch == "aarch64" -> listOfNotNull(aarch64) - arch == "amd64" || arch == "x86_64" -> listOfNotNull(x64) - else -> error("Unsupported architecture: $arch") - } - - tasks.register("cmakeJvmBuild") { - dependsOn(cmakeJvmBuilds) - group = "cmake" - from(cmakeJvmBuilds) - into(binariesFolder.map { it.dir("sqlite") }) } -} -val downloadPowersyncDesktopBinaries = tasks.register("downloadPowersyncDesktopBinaries") { - val coreVersion = libs.versions.powersync.core.get() - val linux_aarch64 = "https://github.com/powersync-ja/powersync-sqlite-core/releases/download/v$coreVersion/libpowersync_aarch64.so" - val linux_x64 = "https://github.com/powersync-ja/powersync-sqlite-core/releases/download/v$coreVersion/libpowersync_x64.so" - val macos_aarch64 = "https://github.com/powersync-ja/powersync-sqlite-core/releases/download/v$coreVersion/libpowersync_aarch64.dylib" - val macos_x64 = "https://github.com/powersync-ja/powersync-sqlite-core/releases/download/v$coreVersion/libpowersync_x64.dylib" - val windows_x64 = "https://github.com/powersync-ja/powersync-sqlite-core/releases/download/v$coreVersion/powersync_x64.dll" - if (binariesAreProvided) { - src(listOf(linux_aarch64, linux_x64, macos_aarch64, macos_x64, windows_x64)) - } else { - val (aarch64, x64) = when { - os.isLinux -> linux_aarch64 to linux_x64 - os.isMacOsX -> macos_aarch64 to macos_x64 - os.isWindows -> null to windows_x64 - else -> error("Unknown operating system: $os") +val downloadPowersyncDesktopBinaries = + tasks.register("downloadPowersyncDesktopBinaries") { + val coreVersion = + libs.versions.powersync.core + .get() + val linux_aarch64 = "https://github.com/powersync-ja/powersync-sqlite-core/releases/download/v$coreVersion/libpowersync_aarch64.so" + val linux_x64 = "https://github.com/powersync-ja/powersync-sqlite-core/releases/download/v$coreVersion/libpowersync_x64.so" + val macos_aarch64 = "https://github.com/powersync-ja/powersync-sqlite-core/releases/download/v$coreVersion/libpowersync_aarch64.dylib" + val macos_x64 = "https://github.com/powersync-ja/powersync-sqlite-core/releases/download/v$coreVersion/libpowersync_x64.dylib" + val windows_x64 = "https://github.com/powersync-ja/powersync-sqlite-core/releases/download/v$coreVersion/powersync_x64.dll" + if (binariesAreProvided) { + src(listOf(linux_aarch64, linux_x64, macos_aarch64, macos_x64, windows_x64)) + } else { + val (aarch64, x64) = + when { + os.isLinux -> linux_aarch64 to linux_x64 + os.isMacOsX -> macos_aarch64 to macos_x64 + os.isWindows -> null to windows_x64 + else -> error("Unknown operating system: $os") + } + val arch = System.getProperty("os.arch") + src( + when { + crossArch -> listOfNotNull(aarch64, x64) + arch == "aarch64" -> listOfNotNull(aarch64) + arch == "amd64" || arch == "x86_64" -> listOfNotNull(x64) + else -> error("Unsupported architecture: $arch") + }, + ) } - val arch = System.getProperty("os.arch") - src(when { - crossArch -> listOfNotNull(aarch64, x64) - arch == "aarch64" -> listOfNotNull(aarch64) - arch == "amd64" || arch == "x86_64" -> listOfNotNull(x64) - else -> error("Unsupported architecture: $arch") - }) + dest(binariesFolder.map { it.dir("powersync") }) + onlyIfModified(true) } - dest(binariesFolder.map { it.dir("powersync") }) - onlyIfModified(true) -} tasks.named(kotlin.jvm().compilations["main"].processResourcesTaskName) { from(getBinaries, downloadPowersyncDesktopBinaries) diff --git a/core/src/macosMain/kotlin/BuildConfig.kt b/core/src/macosMain/kotlin/BuildConfig.kt new file mode 100644 index 00000000..1005f70c --- /dev/null +++ b/core/src/macosMain/kotlin/BuildConfig.kt @@ -0,0 +1,8 @@ +import kotlin.experimental.ExperimentalNativeApi +import kotlin.native.Platform + +@Suppress("EXPECT_ACTUAL_CLASSIFIERS_ARE_IN_BETA_WARNING") +public actual object BuildConfig { + @OptIn(ExperimentalNativeApi::class) + public actual val isDebug: Boolean = Platform.isDebugBinary +} diff --git a/core/src/macosMain/kotlin/com/powersync/DatabaseDriverFactory.macos.kt b/core/src/macosMain/kotlin/com/powersync/DatabaseDriverFactory.macos.kt new file mode 100644 index 00000000..adb40057 --- /dev/null +++ b/core/src/macosMain/kotlin/com/powersync/DatabaseDriverFactory.macos.kt @@ -0,0 +1,132 @@ +package com.powersync + +import app.cash.sqldelight.async.coroutines.synchronous +import app.cash.sqldelight.driver.native.NativeSqliteDriver +import app.cash.sqldelight.driver.native.wrapConnection +import co.touchlab.sqliter.DatabaseConfiguration +import co.touchlab.sqliter.DatabaseConnection +import com.powersync.db.internal.InternalSchema +import com.powersync.sqlite.core.init_powersync_sqlite_extension +import com.powersync.sqlite.core.sqlite3_commit_hook +import com.powersync.sqlite.core.sqlite3_rollback_hook +import com.powersync.sqlite.core.sqlite3_update_hook +import kotlinx.cinterop.ExperimentalForeignApi +import kotlinx.cinterop.MemScope +import kotlinx.cinterop.StableRef +import kotlinx.cinterop.asStableRef +import kotlinx.cinterop.staticCFunction +import kotlinx.cinterop.toKString +import kotlinx.coroutines.CoroutineScope + +@Suppress("EXPECT_ACTUAL_CLASSIFIERS_ARE_IN_BETA_WARNING") +@OptIn(ExperimentalForeignApi::class) +public actual class DatabaseDriverFactory { + private var driver: PsSqlDriver? = null + + init { + init_powersync_sqlite_extension() + } + + @Suppress("unused", "UNUSED_PARAMETER") + private fun updateTableHook( + opType: Int, + databaseName: String, + tableName: String, + rowId: Long, + ) { + driver?.updateTable(tableName) + } + + 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 = + NativeSqliteDriver( + configuration = + DatabaseConfiguration( + name = dbFilename, + version = schema.version.toInt(), + create = { connection -> wrapConnection(connection) { schema.create(it) } }, + lifecycleConfig = + DatabaseConfiguration.Lifecycle( + onCreateConnection = { connection -> + setupSqliteBinding(connection) + wrapConnection(connection) { driver -> + schema.create(driver) + } + }, + onCloseConnection = { connection -> + deregisterSqliteBinding(connection) + }, + ), + ), + ), + ) + return this.driver as PsSqlDriver + } + + private fun setupSqliteBinding(connection: DatabaseConnection) { + val ptr = connection.getDbPointer().getPointer(MemScope()) + + // Register the update hook + sqlite3_update_hook( + ptr, + staticCFunction { usrPtr, updateType, dbName, tableName, rowId -> + val callback = + usrPtr!! + .asStableRef<(Int, String, String, Long) -> Unit>() + .get() + callback( + updateType, + dbName!!.toKString(), + tableName!!.toKString(), + rowId, + ) + }, + StableRef.create(::updateTableHook).asCPointer(), + ) + + // Register transaction hooks + sqlite3_commit_hook( + ptr, + staticCFunction { usrPtr -> + val callback = usrPtr!!.asStableRef<(Boolean) -> Unit>().get() + callback(true) + 0 + }, + StableRef.create(::onTransactionCommit).asCPointer(), + ) + sqlite3_rollback_hook( + ptr, + staticCFunction { usrPtr -> + val callback = usrPtr!!.asStableRef<(Boolean) -> Unit>().get() + callback(false) + }, + StableRef.create(::onTransactionCommit).asCPointer(), + ) + } + + private fun deregisterSqliteBinding(connection: DatabaseConnection) { + val ptr = connection.getDbPointer().getPointer(MemScope()) + sqlite3_update_hook( + ptr, + null, + null, + ) + } +} diff --git a/persistence/build.gradle.kts b/persistence/build.gradle.kts index 75b1bdfa..726109b3 100644 --- a/persistence/build.gradle.kts +++ b/persistence/build.gradle.kts @@ -18,6 +18,8 @@ kotlin { iosX64() iosArm64() iosSimulatorArm64() + macosX64() + macosArm64() explicitApi() @@ -37,7 +39,7 @@ kotlin { api(libs.sqldelight.driver.jdbc) } - iosMain.dependencies { + appleMain.dependencies { api(libs.sqldelight.driver.ios) } }