diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 82b9780e..764a333c 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -16,7 +16,7 @@ jobs: matrix: include: - os: macos-latest - targets: iosSimulatorArm64Test macosArm64Test jvmTest + targets: iosSimulatorArm64Test macosArm64Test watchosSimulatorArm64Test jvmTest - os: ubuntu-latest targets: testDebugUnitTest testReleaseUnitTest jvmTest lintKotlin - os: windows-latest diff --git a/PowerSyncKotlin/build.gradle.kts b/PowerSyncKotlin/build.gradle.kts index 797c14c2..850c46ba 100644 --- a/PowerSyncKotlin/build.gradle.kts +++ b/PowerSyncKotlin/build.gradle.kts @@ -16,6 +16,10 @@ kotlin { iosSimulatorArm64(), macosArm64(), macosX64(), + watchosDeviceArm64(), + watchosArm64(), + watchosSimulatorArm64(), + watchosX64(), ).forEach { it.binaries.framework { baseName = "PowerSyncKotlin" diff --git a/connectors/supabase/build.gradle.kts b/connectors/supabase/build.gradle.kts index 4de82464..485d3fd2 100644 --- a/connectors/supabase/build.gradle.kts +++ b/connectors/supabase/build.gradle.kts @@ -12,7 +12,8 @@ plugins { } kotlin { - powersyncTargets() + // The Supabase KMP project does not support arm64 watchOS builds + powersyncTargets(watchOS = false) targets.withType { compilations.named("main") { compileTaskProvider { diff --git a/core/build.gradle.kts b/core/build.gradle.kts index 87a44af8..51e4a09f 100644 --- a/core/build.gradle.kts +++ b/core/build.gradle.kts @@ -6,6 +6,7 @@ import org.gradle.internal.os.OperatingSystem import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget import org.jetbrains.kotlin.gradle.targets.jvm.tasks.KotlinJvmTest import org.jetbrains.kotlin.gradle.tasks.KotlinTest +import org.jetbrains.kotlin.konan.target.Family plugins { @@ -133,18 +134,16 @@ kotlin { compileTaskProvider { compilerOptions.freeCompilerArgs.add("-Xexport-kdoc") } - } - /* - If we ever need macOS support: - { - binaries.withType().configureEach { - linkTaskProvider.dependsOn(downloadPowersyncDesktopBinaries) - linkerOpts("-lpowersync") - linkerOpts("-L", binariesFolder.map { it.dir("powersync") }.get().asFile.path) + if (this.target.konanTarget.family == Family.WATCHOS) { + // We're linking the core extension statically, which means that we need a cinterop + // to call powersync_init_static + cinterops.create("powersync_static") { + packageName("com.powersync.static") + headers(file("src/watchosMain/powersync_static.h")) + } } } - */ } explicitApi() diff --git a/core/src/appleMain/kotlin/com/powersync/DatabaseDriverFactory.ios.kt b/core/src/appleMain/kotlin/com/powersync/DatabaseDriverFactory.apple.kt similarity index 82% rename from core/src/appleMain/kotlin/com/powersync/DatabaseDriverFactory.ios.kt rename to core/src/appleMain/kotlin/com/powersync/DatabaseDriverFactory.apple.kt index 2066830f..943c3a12 100644 --- a/core/src/appleMain/kotlin/com/powersync/DatabaseDriverFactory.ios.kt +++ b/core/src/appleMain/kotlin/com/powersync/DatabaseDriverFactory.apple.kt @@ -12,6 +12,7 @@ import co.touchlab.sqliter.sqlite3.sqlite3_enable_load_extension import co.touchlab.sqliter.sqlite3.sqlite3_load_extension import co.touchlab.sqliter.sqlite3.sqlite3_rollback_hook import co.touchlab.sqliter.sqlite3.sqlite3_update_hook +import com.powersync.DatabaseDriverFactory.Companion.powerSyncExtensionPath import com.powersync.db.internal.InternalSchema import com.powersync.persistence.driver.NativeSqliteDriver import com.powersync.persistence.driver.wrapConnection @@ -22,6 +23,7 @@ import kotlinx.cinterop.MemScope import kotlinx.cinterop.StableRef import kotlinx.cinterop.alloc import kotlinx.cinterop.asStableRef +import kotlinx.cinterop.free import kotlinx.cinterop.nativeHeap import kotlinx.cinterop.ptr import kotlinx.cinterop.staticCFunction @@ -132,38 +134,9 @@ public actual class DatabaseDriverFactory { connection: DatabaseConnection, driver: DeferredDriver, ) { - val ptr = connection.getDbPointer().getPointer(MemScope()) - val extensionPath = powerSyncExtensionPath - - // Enable extension loading - // We don't disable this after the fact, this should allow users to load their own extensions - // in future. - val enableResult = sqlite3_enable_load_extension(ptr, 1) - if (enableResult != SqliteErrorType.SQLITE_OK.code) { - throw PowerSyncException( - "Could not dynamically load the PowerSync SQLite core extension", - cause = - Exception( - "Call to sqlite3_enable_load_extension failed", - ), - ) - } - - // A place to store a potential error message response - val errMsg = nativeHeap.alloc>() - val result = - sqlite3_load_extension(ptr, extensionPath, "sqlite3_powersync_init", errMsg.ptr) - if (result != SqliteErrorType.SQLITE_OK.code) { - val errorMessage = errMsg.value?.toKString() ?: "Unknown error" - throw PowerSyncException( - "Could not load the PowerSync SQLite core extension", - cause = - Exception( - "Calling sqlite3_load_extension failed with error: $errorMessage", - ), - ) - } + connection.loadPowerSyncSqliteCoreExtension() + val ptr = connection.getDbPointer().getPointer(MemScope()) val driverRef = StableRef.create(driver) sqlite3_update_hook( @@ -221,3 +194,41 @@ public actual class DatabaseDriverFactory { } } } + +internal fun DatabaseConnection.loadPowerSyncSqliteCoreExtensionDynamically() { + val ptr = getDbPointer().getPointer(MemScope()) + val extensionPath = powerSyncExtensionPath + + // Enable extension loading + // We don't disable this after the fact, this should allow users to load their own extensions + // in future. + val enableResult = sqlite3_enable_load_extension(ptr, 1) + if (enableResult != SqliteErrorType.SQLITE_OK.code) { + throw PowerSyncException( + "Could not dynamically load the PowerSync SQLite core extension", + cause = + Exception( + "Call to sqlite3_enable_load_extension failed", + ), + ) + } + + // A place to store a potential error message response + val errMsg = nativeHeap.alloc>() + val result = + sqlite3_load_extension(ptr, extensionPath, "sqlite3_powersync_init", errMsg.ptr) + val resultingError = errMsg.value + nativeHeap.free(errMsg) + if (result != SqliteErrorType.SQLITE_OK.code) { + val errorMessage = resultingError?.toKString() ?: "Unknown error" + throw PowerSyncException( + "Could not load the PowerSync SQLite core extension", + cause = + Exception( + "Calling sqlite3_load_extension failed with error: $errorMessage", + ), + ) + } +} + +internal expect fun DatabaseConnection.loadPowerSyncSqliteCoreExtension() diff --git a/core/src/appleTest/kotlin/com/powersync/DatabaseDriverFactoryTest.kt b/core/src/appleTest/kotlin/com/powersync/DatabaseDriverFactoryTest.kt index c013cd83..01ac23c2 100644 --- a/core/src/appleTest/kotlin/com/powersync/DatabaseDriverFactoryTest.kt +++ b/core/src/appleTest/kotlin/com/powersync/DatabaseDriverFactoryTest.kt @@ -1,10 +1,16 @@ package com.powersync +import kotlin.experimental.ExperimentalNativeApi import kotlin.test.Test class DatabaseDriverFactoryTest { + @OptIn(ExperimentalNativeApi::class) @Test fun findsPowerSyncFramework() { - DatabaseDriverFactory.powerSyncExtensionPath + if (Platform.osFamily != OsFamily.WATCHOS) { + // On watchOS targets, there's no special extension path because we expect to link the + // PowerSync extension statically due to platform restrictions. + DatabaseDriverFactory.powerSyncExtensionPath + } } } diff --git a/core/src/iosMain/kotlin/com/powersync/DatabaseDriverFactory.ios.kt b/core/src/iosMain/kotlin/com/powersync/DatabaseDriverFactory.ios.kt new file mode 100644 index 00000000..2f2c759c --- /dev/null +++ b/core/src/iosMain/kotlin/com/powersync/DatabaseDriverFactory.ios.kt @@ -0,0 +1,7 @@ +package com.powersync + +import co.touchlab.sqliter.DatabaseConnection + +internal actual fun DatabaseConnection.loadPowerSyncSqliteCoreExtension() { + loadPowerSyncSqliteCoreExtensionDynamically() +} 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..2f2c759c --- /dev/null +++ b/core/src/macosMain/kotlin/com/powersync/DatabaseDriverFactory.macos.kt @@ -0,0 +1,7 @@ +package com.powersync + +import co.touchlab.sqliter.DatabaseConnection + +internal actual fun DatabaseConnection.loadPowerSyncSqliteCoreExtension() { + loadPowerSyncSqliteCoreExtensionDynamically() +} diff --git a/core/src/watchosMain/kotlin/com/powersync/DatabaseDriverFactory.watchos.kt b/core/src/watchosMain/kotlin/com/powersync/DatabaseDriverFactory.watchos.kt new file mode 100644 index 00000000..69e644f0 --- /dev/null +++ b/core/src/watchosMain/kotlin/com/powersync/DatabaseDriverFactory.watchos.kt @@ -0,0 +1,17 @@ +package com.powersync + +import co.touchlab.sqliter.DatabaseConnection +import com.powersync.static.powersync_init_static + +internal actual fun DatabaseConnection.loadPowerSyncSqliteCoreExtension() { + val rc = powersync_init_static() + if (rc != 0) { + throw PowerSyncException( + "Could not load the PowerSync SQLite core extension", + cause = + Exception( + "Calling powersync_init_static returned result code $rc", + ), + ) + } +} diff --git a/core/src/watchosMain/powersync_static.h b/core/src/watchosMain/powersync_static.h new file mode 100644 index 00000000..9a1d3560 --- /dev/null +++ b/core/src/watchosMain/powersync_static.h @@ -0,0 +1 @@ +int powersync_init_static(); diff --git a/plugins/build-plugin/src/main/kotlin/SharedBuildPlugin.kt b/plugins/build-plugin/src/main/kotlin/SharedBuildPlugin.kt index e9488485..7cf32739 100644 --- a/plugins/build-plugin/src/main/kotlin/SharedBuildPlugin.kt +++ b/plugins/build-plugin/src/main/kotlin/SharedBuildPlugin.kt @@ -57,42 +57,29 @@ class SharedBuildPlugin : Plugin { .targets .withType() .configureEach { - if (konanTarget.family == Family.IOS && - konanTarget.name.contains( - "simulator", - ) - ) { - binaries - .withType() - .configureEach { - linkTaskProvider.configure { dependsOn(unzipPowersyncFramework) } - linkerOpts("-framework", "powersync-sqlite-core") + val abiName = when(konanTarget.family) { + Family.OSX -> "macos-arm64_x86_64" + // We're testing on simulators + Family.IOS -> "ios-arm64_x86_64-simulator" + Family.WATCHOS -> "watchos-arm64_x86_64-simulator" + else -> return@configureEach + } - val frameworkRoot = - binariesFolder - .map { it.dir("framework/extracted/powersync-sqlite-core.xcframework/ios-arm64_x86_64-simulator") } - .get() - .asFile.path + binaries + .withType() + .configureEach { + linkTaskProvider.configure { dependsOn(unzipPowersyncFramework) } + linkerOpts("-framework", "powersync-sqlite-core") - linkerOpts("-F", frameworkRoot) - linkerOpts("-rpath", frameworkRoot) - } - } else if (konanTarget.family == Family.OSX) { - binaries - .withType() - .configureEach { - linkTaskProvider.configure { dependsOn("unzipPowersyncFramework") } - linkerOpts("-framework", "powersync-sqlite-core") - var frameworkRoot = - binariesFolder - .map { it.dir("framework/extracted/powersync-sqlite-core.xcframework/macos-arm64_x86_64") } - .get() - .asFile.path + val frameworkRoot = + binariesFolder + .map { it.dir("framework/extracted/powersync-sqlite-core.xcframework/$abiName") } + .get() + .asFile.path - linkerOpts("-F", frameworkRoot) - linkerOpts("-rpath", frameworkRoot) - } - } + linkerOpts("-F", frameworkRoot) + linkerOpts("-rpath", frameworkRoot) + } } } } diff --git a/plugins/sonatype/src/main/kotlin/com/powersync/plugins/utils/KmpUtils.kt b/plugins/sonatype/src/main/kotlin/com/powersync/plugins/utils/KmpUtils.kt index c4fc0e83..22d139ab 100644 --- a/plugins/sonatype/src/main/kotlin/com/powersync/plugins/utils/KmpUtils.kt +++ b/plugins/sonatype/src/main/kotlin/com/powersync/plugins/utils/KmpUtils.kt @@ -8,6 +8,7 @@ public fun KotlinTargetContainerWithPresetFunctions.powersyncTargets( native: Boolean = true, jvm: Boolean = true, includeTargetsWithoutComposeSupport: Boolean = true, + watchOS: Boolean = true, ) { if (jvm) { androidTarget { @@ -37,6 +38,14 @@ public fun KotlinTargetContainerWithPresetFunctions.powersyncTargets( if (includeTargetsWithoutComposeSupport) { macosX64() macosArm64() + + if (watchOS) { + watchosDeviceArm64() // aarch64-apple-watchos + watchosArm64() // arm64_32-apple-watchos + + watchosSimulatorArm64() // aarch64-apple-watchos-simulator + watchosX64() // x86_64-apple-watchos-simulator + } } } }