diff --git a/CHANGELOG.md b/CHANGELOG.md index f3123235..064b38bd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,8 +4,10 @@ - Add `PowerSyncDatabase.inMemory` to create an in-memory SQLite database with PowerSync. This may be useful for testing. -- The Supabase connector can now be subclassed to customize how rows are uploaded and how errors are handled. +- The Supabase connector can now be subclassed to customize how rows are uploaded and how errors are + handled. - Experimental support for sync streams. +- [Swift] Added helpers for creating Swift SQLite connection pools. ## 1.6.1 diff --git a/core/src/nativeMain/interop/sqlite3.def b/core/src/nativeMain/interop/sqlite3.def index e90eb403..bb984917 100644 --- a/core/src/nativeMain/interop/sqlite3.def +++ b/core/src/nativeMain/interop/sqlite3.def @@ -1,3 +1,3 @@ headers = sqlite3.h -noStringConversion = sqlite3_prepare_v3 +noStringConversion = sqlite3_prepare_v3,sqlite3session_create diff --git a/core/src/nativeMain/interop/sqlite3.h b/core/src/nativeMain/interop/sqlite3.h index 52447d02..78aef1f8 100644 --- a/core/src/nativeMain/interop/sqlite3.h +++ b/core/src/nativeMain/interop/sqlite3.h @@ -3,36 +3,49 @@ typedef struct sqlite3 sqlite3; typedef struct sqlite3_stmt sqlite3_stmt; +typedef struct sqlite3_session sqlite3_session; +typedef struct sqlite3_changeset_iter sqlite3_changeset_iter; int sqlite3_initialize(); int sqlite3_open_v2(char *filename, sqlite3 **ppDb, int flags, char *zVfs); + int sqlite3_close_v2(sqlite3 *db); // Error handling int sqlite3_extended_result_codes(sqlite3 *db, int onoff); + int sqlite3_extended_errcode(sqlite3 *db); + char *sqlite3_errmsg(sqlite3 *db); + char *sqlite3_errstr(int code); + int sqlite3_error_offset(sqlite3 *db); + void sqlite3_free(void *ptr); // Versions char *sqlite3_libversion(); + char *sqlite3_sourceid(); + int sqlite3_libversion_number(); // Database int sqlite3_get_autocommit(sqlite3 *db); + int sqlite3_db_config(sqlite3 *db, int op, ...); + int sqlite3_load_extension( sqlite3 *db, /* Load the extension into this database connection */ const char *zFile, /* Name of the shared library containing extension */ const char *zProc, /* Entry point. Derived from zFile if 0 */ char **pzErrMsg /* Put error message here if not 0 */ ); -int sqlite3_extended_result_codes(sqlite3*, int onoff); + +int sqlite3_extended_result_codes(sqlite3 *, int onoff); // Statements int sqlite3_prepare16_v3( @@ -43,27 +56,81 @@ int sqlite3_prepare16_v3( sqlite3_stmt **ppStmt, /* OUT: Statement handle */ const void **pzTail /* OUT: Pointer to unused portion of zSql */ ); + int sqlite3_finalize(sqlite3_stmt *pStmt); + int sqlite3_step(sqlite3_stmt *pStmt); + int sqlite3_reset(sqlite3_stmt *pStmt); -int sqlite3_clear_bindings(sqlite3_stmt*); + +int sqlite3_clear_bindings(sqlite3_stmt *); int sqlite3_column_count(sqlite3_stmt *pStmt); + int sqlite3_bind_parameter_count(sqlite3_stmt *pStmt); + char *sqlite3_column_name(sqlite3_stmt *pStmt, int N); int sqlite3_bind_blob64(sqlite3_stmt *pStmt, int index, void *data, uint64_t length, void *destructor); + int sqlite3_bind_double(sqlite3_stmt *pStmt, int index, double data); + int sqlite3_bind_int64(sqlite3_stmt *pStmt, int index, int64_t data); + int sqlite3_bind_null(sqlite3_stmt *pStmt, int index); + int sqlite3_bind_text16(sqlite3_stmt *pStmt, int index, char *data, int length, void *destructor); void *sqlite3_column_blob(sqlite3_stmt *pStmt, int iCol); + double sqlite3_column_double(sqlite3_stmt *pStmt, int iCol); + int64_t sqlite3_column_int64(sqlite3_stmt *pStmt, int iCol); + void *sqlite3_column_text16(sqlite3_stmt *pStmt, int iCol); + int sqlite3_column_bytes(sqlite3_stmt *pStmt, int iCol); + int sqlite3_column_bytes16(sqlite3_stmt *pStmt, int iCol); + int sqlite3_column_type(sqlite3_stmt *pStmt, int iCol); + + +int sqlite3session_create( + sqlite3 *db, /* Database handle */ + const char *zDb, /* Name of db (e.g. "main") */ + sqlite3_session **ppSession /* OUT: New session object */ +); + +int sqlite3session_attach( + sqlite3_session *pSession, /* Session object */ + const char *zTab /* Table name or NULL for all */ +); + +int sqlite3session_changeset( + sqlite3_session *pSession, /* Session object */ + int *pnChangeset, /* OUT: Size of buffer at *ppChangeset */ + void **ppChangeset /* OUT: Buffer containing changeset */ +); + +int sqlite3changeset_start( + sqlite3_changeset_iter **pp, /* OUT: New changeset iterator handle */ + int nChangeset, /* Size of changeset blob in bytes */ + void *pChangeset /* Pointer to blob containing changeset */ +); + +int sqlite3changeset_op( + sqlite3_changeset_iter *pIter, /* Iterator object */ + const char **pzTab, /* OUT: Pointer to table name */ + int *pnCol, /* OUT: Number of columns in table */ + int *pOp, /* OUT: SQLITE_INSERT, DELETE or UPDATE */ + int *pbIndirect /* OUT: True for an 'indirect' change */ +); + +int sqlite3changeset_next(sqlite3_changeset_iter *pIter); + +int sqlite3changeset_finalize(sqlite3_changeset_iter *pIter); + +void sqlite3session_delete(sqlite3_session *pSession); \ No newline at end of file diff --git a/core/src/nativeMain/kotlin/com/powersync/sqlite/Database.kt b/core/src/nativeMain/kotlin/com/powersync/sqlite/Database.kt index a08bf52a..4d24ca75 100644 --- a/core/src/nativeMain/kotlin/com/powersync/sqlite/Database.kt +++ b/core/src/nativeMain/kotlin/com/powersync/sqlite/Database.kt @@ -34,7 +34,7 @@ import kotlinx.cinterop.value * [com.powersync.db.driver.InternalConnectionPool] and called from [kotlinx.coroutines.Dispatchers.IO] * to make these APIs asynchronous. */ -internal class Database( +public class Database( private val ptr: CPointer, ) : SQLiteConnection { override fun inTransaction(): Boolean { @@ -52,22 +52,27 @@ internal class Database( Statement(sql, ptr, stmtPtr.value!!) } - fun loadExtension( + internal fun loadExtension( filename: String, entrypoint: String, - ) = memScoped { - val errorMessagePointer = alloc>() - val resultCode = sqlite3_load_extension(ptr, filename, entrypoint, errorMessagePointer.ptr) - - if (resultCode != 0) { - val errorMessage = errorMessagePointer.value?.toKStringFromUtf8() - if (errorMessage != null) { - sqlite3_free(errorMessagePointer.value) - } + ): Unit = + memScoped { + val errorMessagePointer = alloc>() + val resultCode = + sqlite3_load_extension(ptr, filename, entrypoint, errorMessagePointer.ptr) + + if (resultCode != 0) { + val errorMessage = errorMessagePointer.value?.toKStringFromUtf8() + if (errorMessage != null) { + sqlite3_free(errorMessagePointer.value) + } - throw PowerSyncException("Could not load extension ($resultCode): ${errorMessage ?: "unknown error"}", null) + throw PowerSyncException( + "Could not load extension ($resultCode): ${errorMessage ?: "unknown error"}", + null, + ) + } } - } override fun close() { sqlite3_close_v2(ptr) @@ -79,7 +84,7 @@ internal class Database( } } - companion object { + internal companion object { fun open( path: String, flags: Int, diff --git a/internal/PowerSyncKotlin/build.gradle.kts b/internal/PowerSyncKotlin/build.gradle.kts index 505bf65f..3fa8d56d 100644 --- a/internal/PowerSyncKotlin/build.gradle.kts +++ b/internal/PowerSyncKotlin/build.gradle.kts @@ -48,6 +48,13 @@ kotlin { api(project(":core")) implementation(libs.ktor.client.logging) } + + all { + languageSettings { + optIn("kotlinx.cinterop.ExperimentalForeignApi") + optIn("com.powersync.ExperimentalPowerSyncAPI") + } + } } } diff --git a/internal/PowerSyncKotlin/src/appleMain/kotlin/com/powersync/pool/RawConnectionLease.kt b/internal/PowerSyncKotlin/src/appleMain/kotlin/com/powersync/pool/RawConnectionLease.kt new file mode 100644 index 00000000..12c4024d --- /dev/null +++ b/internal/PowerSyncKotlin/src/appleMain/kotlin/com/powersync/pool/RawConnectionLease.kt @@ -0,0 +1,37 @@ +package com.powersync.pool + +import androidx.sqlite.SQLiteStatement +import com.powersync.db.driver.SQLiteConnectionLease +import com.powersync.sqlite.Database + +internal class RawConnectionLease( + lease: SwiftLeaseAdapter, +) : SQLiteConnectionLease { + private var isCompleted = false + + private var db = Database(lease.pointer) + + private fun checkNotCompleted() { + check(!isCompleted) { "Connection lease already closed" } + } + + override suspend fun isInTransaction(): Boolean = isInTransactionSync() + + override fun isInTransactionSync(): Boolean { + checkNotCompleted() + return db.inTransaction() + } + + override suspend fun usePrepared( + sql: String, + block: (SQLiteStatement) -> R, + ): R = usePreparedSync(sql, block) + + override fun usePreparedSync( + sql: String, + block: (SQLiteStatement) -> R, + ): R { + checkNotCompleted() + return db.prepare(sql).use(block) + } +} diff --git a/internal/PowerSyncKotlin/src/appleMain/kotlin/com/powersync/pool/SwiftPoolAdapter.kt b/internal/PowerSyncKotlin/src/appleMain/kotlin/com/powersync/pool/SwiftPoolAdapter.kt new file mode 100644 index 00000000..877bcf40 --- /dev/null +++ b/internal/PowerSyncKotlin/src/appleMain/kotlin/com/powersync/pool/SwiftPoolAdapter.kt @@ -0,0 +1,64 @@ +package com.powersync.pool + +import cnames.structs.sqlite3 +import com.powersync.PowerSyncException +import com.powersync.sqlite.Database +import io.ktor.utils.io.CancellationException +import kotlinx.cinterop.CPointer +import kotlinx.cinterop.ExperimentalForeignApi + +/** + * The Swift lease will provide a SQLite connection pointer (sqlite3*) which is used in a [Database] + */ +public interface SwiftLeaseAdapter { + public val pointer: CPointer +} + +/** + * A small functional interface to provide a callback with a leased connection. + * We use this structure in order to annotate the callback with exceptions that can be thrown. + */ +public fun interface LeaseCallback { + @Throws(PowerSyncException::class, CancellationException::class) + public fun execute(lease: SwiftLeaseAdapter) +} + +/** + * A small functional interface to provide a callback leases to all connections. + * We use this structure in order to annotate the callback with exceptions that can be thrown. + */ +public fun interface AllLeaseCallback { + @Throws(PowerSyncException::class, CancellationException::class) + public fun execute( + writeLease: SwiftLeaseAdapter, + readLeases: List, + ) +} + +/** + * We only allow synchronous callbacks on the Swift side for leased READ/WRITE connections. + * This adapter here uses synchronous callbacks. + * We also get a SQLite connection pointer (sqlite3*) from Swift side. which is used in a [Database] + * The adapter structure here is focused around easily integrating with a Swift Pool over SKIEE. + */ +public interface SwiftPoolAdapter { + @Throws(PowerSyncException::class, CancellationException::class) + public suspend fun leaseRead(callback: LeaseCallback) + + @Throws(PowerSyncException::class, CancellationException::class) + public suspend fun leaseWrite(callback: LeaseCallback) + + @Throws(PowerSyncException::class, CancellationException::class) + public suspend fun leaseAll(callback: AllLeaseCallback) + + /** + * Links updates from external mutations to PowerSync. + */ + public fun linkExternalUpdates(callback: suspend (Set) -> Unit) + + /** + * Dispose any associated resources with the Pool and PowerSync. + * We don't manage the lifecycle of the pool. + */ + public suspend fun dispose() +} diff --git a/internal/PowerSyncKotlin/src/appleMain/kotlin/com/powersync/pool/SwiftSQLiteConnectionPool.kt b/internal/PowerSyncKotlin/src/appleMain/kotlin/com/powersync/pool/SwiftSQLiteConnectionPool.kt new file mode 100644 index 00000000..0aeb51d0 --- /dev/null +++ b/internal/PowerSyncKotlin/src/appleMain/kotlin/com/powersync/pool/SwiftSQLiteConnectionPool.kt @@ -0,0 +1,108 @@ +package com.powersync.pool + +import co.touchlab.kermit.Logger +import com.powersync.ExperimentalPowerSyncAPI +import com.powersync.PowerSyncDatabase +import com.powersync.db.driver.SQLiteConnectionLease +import com.powersync.db.driver.SQLiteConnectionPool +import com.powersync.db.runWrapped +import com.powersync.db.schema.Schema +import kotlinx.cinterop.ExperimentalForeignApi +import kotlinx.coroutines.DelicateCoroutinesApi +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.runBlocking + +/** + * Accepts a [SwiftPoolAdapter] to implement a [SQLiteConnectionPool] which + * is usable by PowerSync. + */ +public class SwiftSQLiteConnectionPool( + private val adapter: SwiftPoolAdapter, +) : SQLiteConnectionPool { + private val _updates = MutableSharedFlow>(replay = 0) + override val updates: SharedFlow> get() = _updates + + init { + adapter.linkExternalUpdates { tables -> + _updates.emit(tables) + } + } + + override suspend fun read(callback: suspend (SQLiteConnectionLease) -> T): T { + var result: T? = null + /** + * The leaseRead and leaseWrite callbacks don't return values + * since the SKIEE generated version maps to returning Any? Which Swift + * will warn when overriding the method since it's throwable and nil typically + * represents an error in Objective C. + */ + adapter.leaseRead { + runWrapped { + /** + * For GRDB, this should be running inside the callback + * ```swift + * db.write { + * // should be here + * } + * ``` + */ + val lease = + RawConnectionLease(it) + runBlocking { + result = callback(lease) + } + } + } + @Suppress("UNCHECKED_CAST") + return result as T + } + + override suspend fun write(callback: suspend (SQLiteConnectionLease) -> T): T { + var result: T? = null + adapter.leaseWrite { lease -> + runWrapped { + val connectionLease = RawConnectionLease(lease) + runBlocking { + result = callback(connectionLease) + } + } + } + @Suppress("UNCHECKED_CAST") + return result as T + } + + override suspend fun withAllConnections(action: suspend (SQLiteConnectionLease, List) -> R) { + adapter.leaseAll { writerLease, readerLeases -> + runWrapped { + runBlocking { + action( + RawConnectionLease(writerLease), + readerLeases.map { + RawConnectionLease(it) + }, + ) + } + } + } + } + + override suspend fun close() { + adapter.dispose() + } +} + +public fun openPowerSyncWithPool( + pool: SQLiteConnectionPool, + identifier: String, + schema: Schema, + logger: Logger, +): PowerSyncDatabase = + PowerSyncDatabase.Companion.opened( + pool = pool, + scope = GlobalScope, + schema = schema, + identifier = identifier, + logger = logger, + ) diff --git a/internal/PowerSyncKotlin/src/appleMain/kotlin/com/powersync/pool/WithSession.kt b/internal/PowerSyncKotlin/src/appleMain/kotlin/com/powersync/pool/WithSession.kt new file mode 100644 index 00000000..f40783e0 --- /dev/null +++ b/internal/PowerSyncKotlin/src/appleMain/kotlin/com/powersync/pool/WithSession.kt @@ -0,0 +1,163 @@ +package com.powersync.pool + +import cnames.structs.sqlite3 +import cnames.structs.sqlite3_changeset_iter +import cnames.structs.sqlite3_session +import com.powersync.PowerSyncException +import com.powersync.PowerSyncResult +import com.powersync.db.runWrapped +import com.powersync.internal.sqlite3.sqlite3_free +import com.powersync.internal.sqlite3.sqlite3changeset_finalize +import com.powersync.internal.sqlite3.sqlite3changeset_next +import com.powersync.internal.sqlite3.sqlite3changeset_op +import com.powersync.internal.sqlite3.sqlite3changeset_start +import com.powersync.internal.sqlite3.sqlite3session_attach +import com.powersync.internal.sqlite3.sqlite3session_changeset +import com.powersync.internal.sqlite3.sqlite3session_create +import com.powersync.internal.sqlite3.sqlite3session_delete +import kotlinx.cinterop.ByteVar +import kotlinx.cinterop.COpaquePointerVar +import kotlinx.cinterop.CPointer +import kotlinx.cinterop.CPointerVar +import kotlinx.cinterop.IntVar +import kotlinx.cinterop.alloc +import kotlinx.cinterop.memScoped +import kotlinx.cinterop.ptr +import kotlinx.cinterop.toKString +import kotlinx.cinterop.value + +/** + * We typically have a few options for table update hooks: + * 1.) Registering a hook with SQLite + * 2.) Using our Rust core to register update hooks + * 3.) Receiving updates from an external API + * + * In some cases, particularly in the case of GRDB, none of these options are viable. + * GRDB dynamically registers (and unregisters) its own update hooks and its update hook logic + * does not report changes for operations made outside of its own APIs. + * + * 1.) We can't register our own hooks since GRDB might override it or our hook could conflict with GRDB's + * 2.) We can't register hooks due to above + * 3.) The GRDB APIs only report changes if made with their SQLite execution APIs. It's not trivial to implement [com.powersync.db.driver.SQLiteConnectionLease] with their APIs. + * + * This function provides an alternative method of obtaining table changes by using SQLite sessions. + * https://www.sqlite.org/sessionintro.html + * + * We start a session, execute a block of code, and then extract the changeset from the session. + * We then parse the changeset to extract the table names that were modified. + * This approach is more heavyweight than using update hooks, but it works in scenarios where + * update hooks are not currently feasible. + */ +@Throws(PowerSyncException::class) +public fun withSession( + db: CPointer, + onComplete: (PowerSyncResult, Set) -> Unit, + block: () -> PowerSyncResult, +): Unit = + runWrapped { + memScoped { + val sessionPtr = alloc>() + + val rc = + sqlite3session_create( + db, + "main", + sessionPtr.ptr, + ).checkResult("Could not create SQLite session") + + val session = + sessionPtr.value ?: throw PowerSyncException( + "Could not create SQLite session", + cause = Error(), + ) + + try { + // Attach all tables to track changes + sqlite3session_attach( + session, + null, + ).checkResult("Could not attach all tables to session") // null means all tables + + // Execute the block where changes happen + val result = block() + + // Get the changeset + val changesetSizePtr = alloc() + val changesetPtr = alloc() + + sqlite3session_changeset( + session, + changesetSizePtr.ptr, + changesetPtr.ptr, + ).checkResult("Could not get changeset from session") + + val changesetSize = changesetSizePtr.value + val changeset = changesetPtr.value + + if (changesetSize == 0 || changeset == null) { + onComplete(result, emptySet()) + return@memScoped + } + + // Parse the changeset to extract table names + val changedTables = mutableSetOf() + val iterPtr = alloc>() + + sqlite3changeset_start( + iterPtr.ptr, + changesetSize, + changeset, + ).checkResult("Could not start changeset iterator") + + val iter = iterPtr.value + + if (iter == null) { + onComplete(result, emptySet()) + return@memScoped + } + + try { + // Iterate through all changes + while (sqlite3changeset_next(iter) == 100) { + val tableNamePtr = alloc>() + val nColPtr = alloc() + val opPtr = alloc() + val indirectPtr = alloc() + + val opRc = + sqlite3changeset_op( + iter, + tableNamePtr.ptr, + nColPtr.ptr, + opPtr.ptr, + indirectPtr.ptr, + ) + + if (opRc == 0) { + val tableNameCPtr = tableNamePtr.value + if (tableNameCPtr != null) { + val tableName = tableNameCPtr.toKString() + changedTables.add(tableName) + } + } + } + } finally { + sqlite3changeset_finalize(iter) + // Free the changeset memory + sqlite3_free(changeset) + } + + onComplete(result, changedTables.toSet()) + return@memScoped + } finally { + // Clean up the session + sqlite3session_delete(session) + } + } + } + +private fun Int.checkResult(message: String) { + if (this != 0) { + throw PowerSyncException("SQLite error code: $this", cause = Error(message)) + } +} diff --git a/plugins/build-plugin/src/main/kotlin/com/powersync/compile/ClangCompile.kt b/plugins/build-plugin/src/main/kotlin/com/powersync/compile/ClangCompile.kt index e51df1bc..914e7aef 100644 --- a/plugins/build-plugin/src/main/kotlin/com/powersync/compile/ClangCompile.kt +++ b/plugins/build-plugin/src/main/kotlin/com/powersync/compile/ClangCompile.kt @@ -1,12 +1,11 @@ package com.powersync.compile -import kotlin.io.path.Path import org.gradle.api.DefaultTask import org.gradle.api.GradleException -import org.gradle.api.provider.Provider import org.gradle.api.file.DirectoryProperty import org.gradle.api.file.RegularFileProperty import org.gradle.api.provider.Property +import org.gradle.api.provider.Provider import org.gradle.api.provider.ProviderFactory import org.gradle.api.tasks.CacheableTask import org.gradle.api.tasks.Input @@ -18,11 +17,12 @@ import org.gradle.api.tasks.PathSensitivity import org.gradle.api.tasks.TaskAction import org.jetbrains.kotlin.konan.target.KonanTarget import javax.inject.Inject +import kotlin.io.path.Path import kotlin.io.path.absolutePathString import kotlin.io.path.name @CacheableTask -abstract class ClangCompile: DefaultTask() { +abstract class ClangCompile : DefaultTask() { @get:InputFile @get:PathSensitive(PathSensitivity.NONE) abstract val inputFile: RegularFileProperty @@ -41,10 +41,13 @@ abstract class ClangCompile: DefaultTask() { protected abstract val providers: ProviderFactory @get:Input - val xcodeInstallation: Provider get() = providers.exec { - executable("xcode-select") - args("-p") - }.standardOutput.asText + val xcodeInstallation: Provider + get() = + providers + .exec { + executable("xcode-select") + args("-p") + }.standardOutput.asText @TaskAction fun run() { @@ -55,60 +58,73 @@ abstract class ClangCompile: DefaultTask() { } val xcode = Path(xcodePath) - val toolchain = xcode.resolve("Toolchains/XcodeDefault.xctoolchain/usr/bin").absolutePathString() + val toolchain = + xcode.resolve("Toolchains/XcodeDefault.xctoolchain/usr/bin").absolutePathString() - val (llvmTarget, sysRoot) = when (target) { - KonanTarget.IOS_X64 -> "x86_64-apple-ios12.0-simulator" to IOS_SIMULATOR_SDK - KonanTarget.IOS_ARM64 -> "arm64-apple-ios12.0" to IOS_SDK - KonanTarget.IOS_SIMULATOR_ARM64 -> "arm64-apple-ios14.0-simulator" to IOS_SIMULATOR_SDK - KonanTarget.MACOS_ARM64 -> "aarch64-apple-macos" to MACOS_SDK - KonanTarget.MACOS_X64 -> "x86_64-apple-macos" to MACOS_SDK - KonanTarget.WATCHOS_DEVICE_ARM64 -> "aarch64-apple-watchos" to WATCHOS_SDK - KonanTarget.WATCHOS_ARM32 -> "armv7k-apple-watchos" to WATCHOS_SDK - KonanTarget.WATCHOS_ARM64 -> "arm64_32-apple-watchos" to WATCHOS_SDK - KonanTarget.WATCHOS_SIMULATOR_ARM64 -> "aarch64-apple-watchos-simulator" to WATCHOS_SIMULATOR_SDK - KonanTarget.WATCHOS_X64 -> "x86_64-apple-watchos-simulator" to WATCHOS_SIMULATOR_SDK - KonanTarget.TVOS_ARM64 -> "aarch64-apple-tvos" to TVOS_SDK - KonanTarget.TVOS_X64 -> "x86_64-apple-tvos-simulator" to TVOS_SIMULATOR_SDK - KonanTarget.TVOS_SIMULATOR_ARM64 -> "aarch64-apple-tvos-simulator" to TVOS_SIMULATOR_SDK - else -> error("Unexpected target $target") - } + val (llvmTarget, sysRoot) = + when (target) { + KonanTarget.IOS_X64 -> "x86_64-apple-ios12.0-simulator" to IOS_SIMULATOR_SDK + KonanTarget.IOS_ARM64 -> "arm64-apple-ios12.0" to IOS_SDK + KonanTarget.IOS_SIMULATOR_ARM64 -> "arm64-apple-ios14.0-simulator" to IOS_SIMULATOR_SDK + KonanTarget.MACOS_ARM64 -> "aarch64-apple-macos" to MACOS_SDK + KonanTarget.MACOS_X64 -> "x86_64-apple-macos" to MACOS_SDK + KonanTarget.WATCHOS_DEVICE_ARM64 -> "aarch64-apple-watchos" to WATCHOS_SDK + KonanTarget.WATCHOS_ARM32 -> "armv7k-apple-watchos" to WATCHOS_SDK + KonanTarget.WATCHOS_ARM64 -> "arm64_32-apple-watchos" to WATCHOS_SDK + KonanTarget.WATCHOS_SIMULATOR_ARM64 -> "aarch64-apple-watchos-simulator" to WATCHOS_SIMULATOR_SDK + KonanTarget.WATCHOS_X64 -> "x86_64-apple-watchos-simulator" to WATCHOS_SIMULATOR_SDK + KonanTarget.TVOS_ARM64 -> "aarch64-apple-tvos" to TVOS_SDK + KonanTarget.TVOS_X64 -> "x86_64-apple-tvos-simulator" to TVOS_SIMULATOR_SDK + KonanTarget.TVOS_SIMULATOR_ARM64 -> "aarch64-apple-tvos-simulator" to TVOS_SIMULATOR_SDK + else -> error("Unexpected target $target") + } val output = objectFile.get() - providers.exec { - executable = "clang" - args( - "-B${toolchain}", - "-fno-stack-protector", - "-target", - llvmTarget, - "-isysroot", - xcode.resolve(sysRoot).absolutePathString(), - "-fPIC", - "--compile", - "-I${include.get().asFile.absolutePath}", - inputFile.get().asFile.absolutePath, - "-DHAVE_GETHOSTUUID=0", - "-DSQLITE_ENABLE_DBSTAT_VTAB", - "-DSQLITE_ENABLE_FTS5", - "-DSQLITE_ENABLE_RTREE", - "-O3", - "-o", - output.asFile.toPath().name, - ) + providers + .exec { + executable = "clang" + args( + "-B$toolchain", + "-fno-stack-protector", + "-target", + llvmTarget, + "-isysroot", + xcode.resolve(sysRoot).absolutePathString(), + "-fPIC", + "--compile", + "-I${include.get().asFile.absolutePath}", + inputFile.get().asFile.absolutePath, + "-DHAVE_GETHOSTUUID=0", + "-DSQLITE_ENABLE_DBSTAT_VTAB", + "-DSQLITE_ENABLE_FTS5", + "-DSQLITE_ENABLE_RTREE", + // Used by GRDB + "-DSQLITE_ENABLE_SNAPSHOT", + // Used for GRDB update hook like functionality + "-DSQLITE_ENABLE_SESSION", + "-DSQLITE_ENABLE_PREUPDATE_HOOK", + // + "-O3", + "-o", + output.asFile.toPath().name, + ) - workingDir = output.asFile.parentFile - }.result.get() + workingDir = output.asFile.parentFile + }.result + .get() } companion object { const val WATCHOS_SDK = "Platforms/WatchOS.platform/Developer/SDKs/WatchOS.sdk" - const val WATCHOS_SIMULATOR_SDK = "Platforms/WatchSimulator.platform/Developer/SDKs/WatchSimulator.sdk/" + const val WATCHOS_SIMULATOR_SDK = + "Platforms/WatchSimulator.platform/Developer/SDKs/WatchSimulator.sdk/" const val IOS_SDK = "Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk" - const val IOS_SIMULATOR_SDK = "Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk" + const val IOS_SIMULATOR_SDK = + "Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk" const val TVOS_SDK = "Platforms/AppleTVOS.platform/Developer/SDKs/AppleTVOS.sdk" - const val TVOS_SIMULATOR_SDK = "Platforms/AppleTVSimulator.platform/Developer/SDKs/AppleTVSimulator.sdk" + const val TVOS_SIMULATOR_SDK = + "Platforms/AppleTVSimulator.platform/Developer/SDKs/AppleTVSimulator.sdk" const val MACOS_SDK = "Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/" } }