From a5aa4cccc1e62da2e899efd6692eb0e8c6a09dff Mon Sep 17 00:00:00 2001 From: Salomon BRYS Date: Mon, 9 Dec 2024 10:58:53 +0100 Subject: [PATCH 1/3] JVM support --- .github/workflows/deploy.yml | 63 +++++-- .github/workflows/{gradle.yml => test.yml} | 0 PowerSyncKotlin/build.gradle.kts | 7 +- compose/build.gradle.kts | 2 + ...t => DatabaseDriverFactory.compose.jvm.kt} | 0 .../DatabaseDriverFactory.compose.android.kt | 11 ++ connectors/supabase/build.gradle.kts | 8 +- core/.gitignore | 1 + core/build.gradle.kts | 151 +++++++++++++++- core/src/androidMain/cpp/CMakeLists.txt | 2 +- core/src/androidMain/kotlin/BuildConfig.kt | 1 + core/src/commonMain/kotlin/BuildConfig.kt | 1 + .../powersync/db/internal/InternalSchema.kt | 6 +- core/src/iosMain/kotlin/BuildConfig.kt | 1 + core/src/jvmMain/cpp/CMakeLists.txt | 71 ++++++++ core/src/jvmMain/kotlin/BuildConfig.kt | 12 ++ .../powersync/DatabaseDriverFactory.jvm.kt | 75 ++++++++ .../kotlin/com/powersync/ExtractLib.kt | 39 ++++ .../com/powersync/PSJdbcSqliteDriver.kt | 171 ++++++++++++++++++ .../cpp/sqlite_bindings.cpp | 21 +-- demos/hello-powersync/.gitignore | 1 + demos/hello-powersync/README.md | 8 +- .../composeApp/build.gradle.kts | 10 + .../kotlin/com/powersync/demos/main.kt | 8 + demos/hello-powersync/iosApp/Podfile.lock | 2 +- .../iosApp/iosApp.xcodeproj/project.pbxproj | 18 -- demos/hello-powersync/settings.gradle.kts | 1 + gradle/libs.versions.toml | 8 +- persistence/build.gradle.kts | 8 +- 29 files changed, 637 insertions(+), 70 deletions(-) rename .github/workflows/{gradle.yml => test.yml} (100%) rename compose/src/androidMain/kotlin/com/powersync/compose/{DatabaseDriverFactory.compose.android.kt => DatabaseDriverFactory.compose.jvm.kt} (100%) create mode 100644 compose/src/jvmMain/kotlin/com/powersync/compose/DatabaseDriverFactory.compose.android.kt create mode 100644 core/.gitignore create mode 100644 core/src/jvmMain/cpp/CMakeLists.txt create mode 100644 core/src/jvmMain/kotlin/BuildConfig.kt create mode 100644 core/src/jvmMain/kotlin/com/powersync/DatabaseDriverFactory.jvm.kt create mode 100644 core/src/jvmMain/kotlin/com/powersync/ExtractLib.kt create mode 100644 core/src/jvmMain/kotlin/com/powersync/PSJdbcSqliteDriver.kt rename core/src/{androidMain => jvmNative}/cpp/sqlite_bindings.cpp (80%) create mode 100644 demos/hello-powersync/composeApp/src/jvmMain/kotlin/com/powersync/demos/main.kt diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 3a3f09fd..cc90025d 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -6,25 +6,42 @@ permissions: contents: read jobs: - build: - uses: ./.github/workflows/gradle.yml - deploy: - needs: build + test: + uses: ./.github/workflows/test.yml + build-native: + name: Build native lib strategy: matrix: - include: - - target: publishAllPublicationsToSonatypeRepository - os: macos-latest -# FIXME: Our custom gradle plugin does not currently setup platform specific tasks -# - target: publishIosArm64PublicationToSonatypeRepository -# os: macos-latest -# - target: publishAndroidDebugPublicationToSonatypeRepository -# os: ubuntu-latest -# - target: publishAndroidReleasePublicationToSonatypeRepository -# os: ubuntu-latest -# - target: publishKotlinMultiplatformPublicationToSonatypeRepository -# os: ubuntu-latest + os: [macos-latest, ubuntu-latest, windows-latest] runs-on: ${{ matrix.os }} + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Validate Gradle Wrapper + uses: gradle/wrapper-validation-action@v1 + - name: Set up JDK 17 + uses: actions/setup-java@v3 + with: + java-version: '17' + distribution: 'temurin' + - name: Set up Gradle + uses: gradle/actions/setup-gradle@v3 + - name: Install cross-compiler + if: ${{ matrix.os == 'ubuntu-latest' }} + run: sudo apt install -y gcc-aarch64-linux-gnu g++-aarch64-linux-gnu + - name: Build native lib + run: | + ./gradlew \ + -PGITHUB_PUBLISH_TOKEN=${{ secrets.GITHUB_TOKEN }} \ + :core:cmakeJvmBuild + - name: Upload build + uses: actions/upload-artifact@v4 + with: + name: binaries-${{ matrix.os }} + path: core/build/binaries/desktop/sqlite/ + deploy: + needs: [test, build-native] + runs-on: macos-latest steps: - uses: actions/checkout@v4 - name: Validate Gradle Wrapper @@ -39,20 +56,26 @@ jobs: with: java-version: '17' distribution: 'temurin' - - name: Set up Gradle uses: gradle/actions/setup-gradle@v3 - + - name: Download native binaries + uses: actions/download-artifact@v4 + with: + path: core/binaries/desktop + merge-multiple: true + - name: Display downloaded files + run: ls -lR core/binaries/desktop - name: Gradle publish run: | ./gradlew \ - ${{ matrix.target }} \ -PGITHUB_PUBLISH_TOKEN="${{ secrets.GITHUB_TOKEN }}" \ -PsigningInMemoryKey="${{ secrets.SIGNING_KEY }}" \ -PsigningInMemoryKeyId="${{ secrets.SIGNING_KEY_ID }}" \ -PsigningInMemoryKeyPassword="${{ secrets.SIGNING_PASSWORD }}" \ -PcentralPortal.username="${{secrets.SONATYPE_USERNAME}}" \ - -PcentralPortal.password="${{secrets.SONATYPE_PASSWORD}}" + -PcentralPortal.password="${{secrets.SONATYPE_PASSWORD}}" \ + -Ppowersync.binaries.provided="true" + publishAllPublicationsToSonatypeRepository # This will change Package.swift in Github packages to direct to new maven central KMMBridge zip file call-kmmbridge-publish: needs: deploy diff --git a/.github/workflows/gradle.yml b/.github/workflows/test.yml similarity index 100% rename from .github/workflows/gradle.yml rename to .github/workflows/test.yml diff --git a/PowerSyncKotlin/build.gradle.kts b/PowerSyncKotlin/build.gradle.kts index 949c8fa0..2e8d2589 100644 --- a/PowerSyncKotlin/build.gradle.kts +++ b/PowerSyncKotlin/build.gradle.kts @@ -1,6 +1,7 @@ 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 @@ -28,8 +29,10 @@ kotlin { explicitApi() targets.withType { - compilations.getByName("main") { - compilerOptions.options.freeCompilerArgs.add("-Xexport-kdoc") + compilations.named("main") { + compileTaskProvider { + compilerOptions.freeCompilerArgs.add("-Xexport-kdoc") + } } } diff --git a/compose/build.gradle.kts b/compose/build.gradle.kts index 7fca5ded..c6dbcbb4 100644 --- a/compose/build.gradle.kts +++ b/compose/build.gradle.kts @@ -14,6 +14,8 @@ kotlin { publishLibraryVariants("release", "debug") } + jvm() + iosX64() iosArm64() iosSimulatorArm64() diff --git a/compose/src/androidMain/kotlin/com/powersync/compose/DatabaseDriverFactory.compose.android.kt b/compose/src/androidMain/kotlin/com/powersync/compose/DatabaseDriverFactory.compose.jvm.kt similarity index 100% rename from compose/src/androidMain/kotlin/com/powersync/compose/DatabaseDriverFactory.compose.android.kt rename to compose/src/androidMain/kotlin/com/powersync/compose/DatabaseDriverFactory.compose.jvm.kt diff --git a/compose/src/jvmMain/kotlin/com/powersync/compose/DatabaseDriverFactory.compose.android.kt b/compose/src/jvmMain/kotlin/com/powersync/compose/DatabaseDriverFactory.compose.android.kt new file mode 100644 index 00000000..72a33814 --- /dev/null +++ b/compose/src/jvmMain/kotlin/com/powersync/compose/DatabaseDriverFactory.compose.android.kt @@ -0,0 +1,11 @@ +package com.powersync.compose + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import com.powersync.DatabaseDriverFactory + +@Composable +public actual fun rememberDatabaseDriverFactory(): DatabaseDriverFactory = + remember { + DatabaseDriverFactory() + } diff --git a/connectors/supabase/build.gradle.kts b/connectors/supabase/build.gradle.kts index ee1ac4cb..fb1f282e 100644 --- a/connectors/supabase/build.gradle.kts +++ b/connectors/supabase/build.gradle.kts @@ -14,9 +14,13 @@ kotlin { publishLibraryVariants("release", "debug") } + jvm() + targets.withType { - compilations.getByName("main") { - compilerOptions.options.freeCompilerArgs.add("-Xexport-kdoc") + compilations.named("main") { + compileTaskProvider { + compilerOptions.freeCompilerArgs.add("-Xexport-kdoc") + } } } diff --git a/core/.gitignore b/core/.gitignore new file mode 100644 index 00000000..09328be3 --- /dev/null +++ b/core/.gitignore @@ -0,0 +1 @@ +binaries/ diff --git a/core/build.gradle.kts b/core/build.gradle.kts index 97a8a309..37fb657b 100644 --- a/core/build.gradle.kts +++ b/core/build.gradle.kts @@ -1,5 +1,7 @@ +import app.cash.sqldelight.core.capitalize import com.powersync.plugins.sonatype.setupGithubRepository import de.undercouch.gradle.tasks.download.Download +import org.gradle.internal.os.OperatingSystem import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget plugins { @@ -13,12 +15,12 @@ plugins { alias(libs.plugins.mokkery) } -val sqliteVersion = "3450000" +val sqliteVersion = "3450200" val sqliteReleaseYear = "2024" val sqliteSrcFolder = project.layout.buildDirectory - .dir("interop/sqlite") + .dir("native/sqlite") .get() val downloadSQLiteSources by tasks.registering(Download::class) { @@ -50,8 +52,13 @@ val unzipSQLiteSources by tasks.registering(Copy::class) { val buildCInteropDef by tasks.registering { dependsOn(unzipSQLiteSources) + val interopFolder = + project.layout.buildDirectory + .dir("interop/sqlite") + .get() + val cFile = sqliteSrcFolder.file("sqlite3.c").asFile - val defFile = sqliteSrcFolder.file("sqlite3.def").asFile + val defFile = interopFolder.file("sqlite3.def").asFile doFirst { defFile.writeText( @@ -69,18 +76,21 @@ kotlin { androidTarget { publishLibraryVariants("release", "debug") } + jvm() iosX64() iosArm64() iosSimulatorArm64() targets.withType { - compilations.getByName("main") { - compilerOptions.options.freeCompilerArgs.add("-Xexport-kdoc") + compilations.named("main") { + compileTaskProvider { + compilerOptions.freeCompilerArgs.add("-Xexport-kdoc") + } cinterops.create("sqlite") { val cInteropTask = tasks[interopProcessingTaskName] cInteropTask.dependsOn(buildCInteropDef) - defFile = + definitionFile = buildCInteropDef .get() .outputs.files.singleFile @@ -118,6 +128,11 @@ kotlin { implementation(libs.ktor.client.okhttp) } + jvmMain.dependencies { + implementation(libs.ktor.client.okhttp) + implementation(libs.sqlite.jdbc) + } + iosMain.dependencies { implementation(libs.ktor.client.ios) } @@ -159,6 +174,7 @@ android { .get() .toInt() + @Suppress("UnstableApiUsage") externalNativeBuild { cmake { arguments.addAll( @@ -177,6 +193,129 @@ android { } } +val os = OperatingSystem.current() +val binariesAreProvided = project.findProperty("powersync.binaries.provided") == "true" +val binariesFolder = + project.layout.buildDirectory + .dir("binaries/desktop") + +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 + + 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 = "cmake" + 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 = "cmake" + args("--build", ".", "--config", "Release") + } + + return cmakeBuild + } + + val cmakeJvmBuilds = when { + os.isMacOsX -> listOf( + registerCMakeTasks("aarch64", "CMAKE_OSX_ARCHITECTURES=arm64"), + registerCMakeTasks("x64", "CMAKE_OSX_ARCHITECTURES=x86_64"), + ) + os.isLinux -> listOf( + registerCMakeTasks("aarch64", "CMAKE_C_COMPILER=aarch64-linux-gnu-gcc", "CMAKE_CXX_COMPILER=aarch64-linux-gnu-g++"), + registerCMakeTasks( + "x64", "CMAKE_C_COMPILER=x86_64-linux-gnu-gcc", "CMAKE_CXX_COMPILER=x86_64-linux-gnu-g++"), + ) + os.isWindows -> listOf( + registerCMakeTasks("x64"), + ) + else -> error("Unknown operating system: $os") + } + + 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 = listOf( + "https://github.com/powersync-ja/powersync-sqlite-core/releases/download/v$coreVersion/libpowersync_aarch64.so", + "https://github.com/powersync-ja/powersync-sqlite-core/releases/download/v$coreVersion/libpowersync_x64.so", + ) + val mac = listOf( + "https://github.com/powersync-ja/powersync-sqlite-core/releases/download/v$coreVersion/libpowersync_aarch64.dylib", + "https://github.com/powersync-ja/powersync-sqlite-core/releases/download/v$coreVersion/libpowersync_x64.dylib", + ) + val windows = listOf( + "https://github.com/powersync-ja/powersync-sqlite-core/releases/download/v$coreVersion/powersync_x64.dll", + ) + src(when { + binariesAreProvided -> linux + mac + windows + else -> when { + os.isLinux -> linux + os.isMacOsX -> mac + os.isWindows -> windows + else -> error("Unknown operating system: $os") + } + }) + dest(binariesFolder.map { it.dir("powersync") }) + onlyIfModified(true) +} + +tasks.named(kotlin.jvm().compilations["main"].processResourcesTaskName) { + from(getBinaries, downloadPowersyncDesktopBinaries) +} + afterEvaluate { val buildTasks = tasks.matching { diff --git a/core/src/androidMain/cpp/CMakeLists.txt b/core/src/androidMain/cpp/CMakeLists.txt index 648fa833..c80f14dd 100644 --- a/core/src/androidMain/cpp/CMakeLists.txt +++ b/core/src/androidMain/cpp/CMakeLists.txt @@ -8,7 +8,7 @@ set(BUILD_DIR ${CMAKE_SOURCE_DIR}/build) add_library( ${PACKAGE_NAME} SHARED - sqlite_bindings.cpp + ../../jvmNative/cpp/sqlite_bindings.cpp "${SQLITE3_SRC_DIR}/sqlite3.c" ) diff --git a/core/src/androidMain/kotlin/BuildConfig.kt b/core/src/androidMain/kotlin/BuildConfig.kt index e7af1cc1..08f66eec 100644 --- a/core/src/androidMain/kotlin/BuildConfig.kt +++ b/core/src/androidMain/kotlin/BuildConfig.kt @@ -1,3 +1,4 @@ +@Suppress("EXPECT_ACTUAL_CLASSIFIERS_ARE_IN_BETA_WARNING") public actual object BuildConfig { public actual val isDebug: Boolean get() = com.powersync.BuildConfig.DEBUG diff --git a/core/src/commonMain/kotlin/BuildConfig.kt b/core/src/commonMain/kotlin/BuildConfig.kt index 38e47877..599450e5 100644 --- a/core/src/commonMain/kotlin/BuildConfig.kt +++ b/core/src/commonMain/kotlin/BuildConfig.kt @@ -1,3 +1,4 @@ +@Suppress("EXPECT_ACTUAL_CLASSIFIERS_ARE_IN_BETA_WARNING") public expect object BuildConfig { public val isDebug: Boolean } diff --git a/core/src/commonMain/kotlin/com/powersync/db/internal/InternalSchema.kt b/core/src/commonMain/kotlin/com/powersync/db/internal/InternalSchema.kt index a0c491fd..4625246f 100644 --- a/core/src/commonMain/kotlin/com/powersync/db/internal/InternalSchema.kt +++ b/core/src/commonMain/kotlin/com/powersync/db/internal/InternalSchema.kt @@ -9,14 +9,12 @@ internal object InternalSchema : SqlSchema> { override val version: Long get() = 1 - override fun create(driver: SqlDriver): QueryResult.AsyncValue = - QueryResult.AsyncValue {} + override fun create(driver: SqlDriver): QueryResult.AsyncValue = QueryResult.AsyncValue {} override fun migrate( driver: SqlDriver, oldVersion: Long, newVersion: Long, vararg callbacks: AfterVersion, - ): QueryResult.AsyncValue = - QueryResult.AsyncValue {} + ): QueryResult.AsyncValue = QueryResult.AsyncValue {} } diff --git a/core/src/iosMain/kotlin/BuildConfig.kt b/core/src/iosMain/kotlin/BuildConfig.kt index 2d264663..1005f70c 100644 --- a/core/src/iosMain/kotlin/BuildConfig.kt +++ b/core/src/iosMain/kotlin/BuildConfig.kt @@ -1,6 +1,7 @@ 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/jvmMain/cpp/CMakeLists.txt b/core/src/jvmMain/cpp/CMakeLists.txt new file mode 100644 index 00000000..1e4b24c5 --- /dev/null +++ b/core/src/jvmMain/cpp/CMakeLists.txt @@ -0,0 +1,71 @@ +cmake_minimum_required(VERSION 3.18.1) + +project(powersync-sqlite) + +if(NOT SQLITE3_SRC_DIR) + set(SQLITE3_SRC_DIR "../../../build/native/sqlite") +endif() + +if(SUFFIX) + set(PACKAGE_NAME "powersync-sqlite_${SUFFIX}") +else() + message(FATAL_ERROR "SUFFIX not set") +endif() + +set(CMAKE_LIBRARY_OUTPUT_DIRECTORY output) +set(CMAKE_RUNTIME_OUTPUT_DIRECTORY output) + +set(CMAKE_POSITION_INDEPENDENT_CODE ON) + +find_package(JNI REQUIRED) +find_package(Threads REQUIRED) + +include_directories(${SQLITE3_SRC_DIR}) +include_directories(${JNI_INCLUDE_DIRS}) + +add_library(sqlite3 STATIC + "${SQLITE3_SRC_DIR}/sqlite3.c" +) + +# Flags used by the Xerial SQlite JDBC driver. Using the same flags ensures maximum compatibility. +# See https://github.com/xerial/sqlite-jdbc/blob/533efaec2b1558d67e23ed496e6c32d505acd812/Makefile#L68 +target_compile_definitions( + sqlite3 + PUBLIC SQLITE_ENABLE_LOAD_EXTENSION=1 + PUBLIC SQLITE_HAVE_ISNAN + PUBLIC HAVE_USLEEP=1 + PUBLIC SQLITE_ENABLE_COLUMN_METADATA + PUBLIC SQLITE_CORE + PUBLIC SQLITE_ENABLE_FTS3 + PUBLIC SQLITE_ENABLE_FTS3_PARENTHESIS + PUBLIC SQLITE_ENABLE_FTS5 + PUBLIC SQLITE_ENABLE_RTREE + PUBLIC SQLITE_ENABLE_STAT4 + PUBLIC SQLITE_ENABLE_DBSTAT_VTAB + PUBLIC SQLITE_ENABLE_MATH_FUNCTIONS + PUBLIC SQLITE_THREADSAFE=1 + PUBLIC SQLITE_DEFAULT_MEMSTATUS=0 + PUBLIC SQLITE_DEFAULT_FILE_PERMISSIONS=0666 + PUBLIC SQLITE_MAX_VARIABLE_NUMBER=250000 + PUBLIC SQLITE_MAX_MMAP_SIZE=1099511627776 + PUBLIC SQLITE_MAX_LENGTH=2147483647 + PUBLIC SQLITE_MAX_COLUMN=32767 + PUBLIC SQLITE_MAX_SQL_LENGTH=1073741824 + PUBLIC SQLITE_MAX_FUNCTION_ARG=127 + PUBLIC SQLITE_MAX_ATTACHED=125 + PUBLIC SQLITE_MAX_PAGE_COUNT=4294967294 + PUBLIC SQLITE_DISABLE_PAGECACHE_OVERFLOW_STATS +) + +add_library( + ${PACKAGE_NAME} + SHARED + ../../jvmNative/cpp/sqlite_bindings.cpp +) + +target_link_libraries( + ${PACKAGE_NAME} + PRIVATE + sqlite3 + ${CMAKE_THREAD_LIBS_INIT} +) \ No newline at end of file diff --git a/core/src/jvmMain/kotlin/BuildConfig.kt b/core/src/jvmMain/kotlin/BuildConfig.kt new file mode 100644 index 00000000..c3260bed --- /dev/null +++ b/core/src/jvmMain/kotlin/BuildConfig.kt @@ -0,0 +1,12 @@ +@Suppress("EXPECT_ACTUAL_CLASSIFIERS_ARE_IN_BETA_WARNING") +public actual object BuildConfig { + /* + To debug on the JVM, you can: + - Set the com.powersync.debug property with System.setProperty("com.powersync.debug", true) BEFORE calling any powersync API. + - Start your java program with the -Dcom.powersync.debug=true command line argument. + - Set the POWERSYNC_JVM_DEBUG environment variable to "true" before starting your program. + */ + public actual val isDebug: Boolean = + System.getProperty("com.powersync.debug") == "true" || + System.getenv("POWERSYNC_JVM_DEBUG") == "true" +} diff --git a/core/src/jvmMain/kotlin/com/powersync/DatabaseDriverFactory.jvm.kt b/core/src/jvmMain/kotlin/com/powersync/DatabaseDriverFactory.jvm.kt new file mode 100644 index 00000000..684baf3e --- /dev/null +++ b/core/src/jvmMain/kotlin/com/powersync/DatabaseDriverFactory.jvm.kt @@ -0,0 +1,75 @@ +package com.powersync + +import app.cash.sqldelight.async.coroutines.synchronous +import com.powersync.db.internal.InternalSchema +import kotlinx.coroutines.CoroutineScope +import java.nio.file.Path +import kotlin.io.path.absolutePathString + +@Suppress("EXPECT_ACTUAL_CLASSIFIERS_ARE_IN_BETA_WARNING", "SqlNoDataSourceInspection") +public actual class DatabaseDriverFactory { + private var driver: PsSqlDriver? = null + + private external fun setupSqliteBinding() + + // Used in native + @Suppress("unused") + private fun onTableUpdate(tableName: String) { + driver?.updateTable(tableName) + } + + // Used in native + @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() + + val driver = + PSJdbcSqliteDriver( + url = "jdbc:sqlite:$dbFilename", + schema = schema, + ) + // Generates SQLITE_BUSY errors +// driver.enableWriteAheadLogging() + driver.loadExtensions( + powersyncExtension to "sqlite3_powersync_init", + jniExtension to "powersync_init", + ) + + setupSqliteBinding() + + this.driver = + PsSqlDriver( + scope = scope, + driver = driver, + ) + + return this.driver!! + } + + public companion object { + private val jniExtension: Path + private val powersyncExtension: Path + + init { + val nativeLib = extractLib("powersync-sqlite") + @Suppress("UnsafeDynamicallyLoadedCode") + System.load(nativeLib.absolutePathString()) + jniExtension = nativeLib + + powersyncExtension = extractLib("powersync") + } + } +} diff --git a/core/src/jvmMain/kotlin/com/powersync/ExtractLib.kt b/core/src/jvmMain/kotlin/com/powersync/ExtractLib.kt new file mode 100644 index 00000000..021aa7d8 --- /dev/null +++ b/core/src/jvmMain/kotlin/com/powersync/ExtractLib.kt @@ -0,0 +1,39 @@ +package com.powersync + +import java.nio.file.Path +import kotlin.io.path.createTempFile +import kotlin.io.path.deleteIfExists +import kotlin.io.path.outputStream + +private class R + +internal fun extractLib(fileName: String): Path { + val os = System.getProperty("os.name").lowercase() + val (prefix, extension) = + when { + os.contains("nux") || os.contains("nix") || os.contains("aix") -> "lib" to "so" + os.contains("mac") -> "lib" to "dylib" + os.contains("win") -> "" to "dll" + else -> error("Unsupported OS: $os") + } + + val arch = + when (val sysArch = System.getProperty("os.arch")) { + "aarch64" -> "aarch64" + "amd64" -> "x64" + else -> error("Unknown architecture: $sysArch") + } + + val path = "/$prefix${fileName}_$arch.$extension" + + val tmpPath = createTempFile("$prefix$fileName", ".$extension") + Runtime.getRuntime().addShutdownHook(Thread { tmpPath.deleteIfExists() }) + + (R::class.java.getResourceAsStream(path) ?: error("Resource $path not found")).use { input -> + tmpPath.outputStream().use { output -> + input.copyTo(output) + } + } + + return tmpPath +} diff --git a/core/src/jvmMain/kotlin/com/powersync/PSJdbcSqliteDriver.kt b/core/src/jvmMain/kotlin/com/powersync/PSJdbcSqliteDriver.kt new file mode 100644 index 00000000..a043cd51 --- /dev/null +++ b/core/src/jvmMain/kotlin/com/powersync/PSJdbcSqliteDriver.kt @@ -0,0 +1,171 @@ +package com.powersync + +import app.cash.sqldelight.Query +import app.cash.sqldelight.Transacter +import app.cash.sqldelight.db.AfterVersion +import app.cash.sqldelight.db.QueryResult +import app.cash.sqldelight.db.SqlCursor +import app.cash.sqldelight.db.SqlDriver +import app.cash.sqldelight.db.SqlPreparedStatement +import app.cash.sqldelight.db.SqlSchema +import app.cash.sqldelight.driver.jdbc.JdbcPreparedStatement +import org.sqlite.SQLiteConnection +import java.nio.file.Path +import java.sql.DriverManager +import java.sql.PreparedStatement +import java.util.Properties +import kotlin.io.path.absolutePathString + +@Suppress("SqlNoDataSourceInspection", "SqlSourceToSinkFlow") +internal class PSJdbcSqliteDriver( + url: String, + properties: Properties = Properties(), +) : SqlDriver { + private val listeners = linkedMapOf>() + + override fun addListener( + vararg queryKeys: String, + listener: Query.Listener, + ) { + synchronized(listeners) { + queryKeys.forEach { + listeners.getOrPut(it, { linkedSetOf() }).add(listener) + } + } + } + + override fun removeListener( + vararg queryKeys: String, + listener: Query.Listener, + ) { + synchronized(listeners) { + queryKeys.forEach { + listeners[it]?.remove(listener) + } + } + } + + override fun notifyListeners(vararg queryKeys: String) { + val listenersToNotify = linkedSetOf() + synchronized(listeners) { + queryKeys.forEach { listeners[it]?.let(listenersToNotify::addAll) } + } + listenersToNotify.forEach(Query.Listener::queryResultsChanged) + } + + private val connection: SQLiteConnection = DriverManager.getConnection(url, properties) as SQLiteConnection + + private var transaction: Transaction? = null + + private inner class Transaction( + override val enclosingTransaction: Transaction?, + ) : Transacter.Transaction() { + init { + connection.prepareStatement("BEGIN TRANSACTION").use(PreparedStatement::execute) + } + + override fun endTransaction(successful: Boolean): QueryResult { + if (enclosingTransaction == null) { + if (successful) { + connection.prepareStatement("END TRANSACTION").use(PreparedStatement::execute) + } else { + connection.prepareStatement("ROLLBACK TRANSACTION").use(PreparedStatement::execute) + } + } + transaction = enclosingTransaction + return QueryResult.Unit + } + } + + override fun newTransaction(): QueryResult { + val newTransaction = Transaction(transaction) + transaction = newTransaction + return QueryResult.Value(newTransaction) + } + + override fun close() { + connection.close() + } + + override fun currentTransaction(): Transacter.Transaction? = transaction + + @Synchronized + override fun execute( + identifier: Int?, + sql: String, + parameters: Int, + binders: (SqlPreparedStatement.() -> Unit)?, + ): QueryResult = + QueryResult.Value( + connection.prepareStatement(sql).use { + val stmt = JdbcPreparedStatement(it) + binders?.invoke(stmt) + stmt.execute() + }, + ) + + @Synchronized + override fun executeQuery( + identifier: Int?, + sql: String, + mapper: (SqlCursor) -> QueryResult, + parameters: Int, + binders: (SqlPreparedStatement.() -> Unit)?, + ): QueryResult = + connection.prepareStatement(sql).use { + val stmt = JdbcPreparedStatement(it) + binders?.invoke(stmt) + stmt.executeQuery(mapper) + } + + internal fun loadExtensions(vararg extensions: Pair) { + connection.database.enable_load_extension(true) + extensions.forEach { (path, entryPoint) -> + val executed = + connection.prepareStatement("SELECT load_extension(?, ?);").use { statement -> + statement.setString(1, path.absolutePathString()) + statement.setString(2, entryPoint) + statement.execute() + } + check(executed) { "load_extension(\"${path.absolutePathString()}\", \"${entryPoint}\") failed" } + } + connection.database.enable_load_extension(false) + } + + internal fun enableWriteAheadLogging() { + val executed = connection.prepareStatement("PRAGMA journal_mode=WAL;").execute() + check(executed) { "journal_mode=WAL failed" } + } +} + +internal fun PSJdbcSqliteDriver( + url: String, + properties: Properties = Properties(), + schema: SqlSchema>, + migrateEmptySchema: Boolean = false, + vararg callbacks: AfterVersion, +): PSJdbcSqliteDriver { + val driver = PSJdbcSqliteDriver(url, properties) + val version = driver.getVersion() + + if (version == 0L && !migrateEmptySchema) { + schema.create(driver).value + driver.setVersion(schema.version) + } else if (version < schema.version) { + schema.migrate(driver, version, schema.version, *callbacks).value + driver.setVersion(schema.version) + } + + return driver +} + +private fun PSJdbcSqliteDriver.getVersion(): Long { + val mapper = { cursor: SqlCursor -> + QueryResult.Value(if (cursor.next().value) cursor.getLong(0) else null) + } + return executeQuery(null, "PRAGMA user_version", mapper, 0, null).value ?: 0L +} + +private fun PSJdbcSqliteDriver.setVersion(version: Long) { + execute(null, "PRAGMA user_version = $version", 0, null).value +} diff --git a/core/src/androidMain/cpp/sqlite_bindings.cpp b/core/src/jvmNative/cpp/sqlite_bindings.cpp similarity index 80% rename from core/src/androidMain/cpp/sqlite_bindings.cpp rename to core/src/jvmNative/cpp/sqlite_bindings.cpp index 81ccb1df..9847b8fa 100644 --- a/core/src/androidMain/cpp/sqlite_bindings.cpp +++ b/core/src/jvmNative/cpp/sqlite_bindings.cpp @@ -1,7 +1,7 @@ #include -#include #include #include +#include typedef struct context { JavaVM *javaVM; @@ -12,7 +12,8 @@ Context g_ctx; extern "C" { -JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved) { +JNIEXPORT +jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved) { JNIEnv *env; memset(&g_ctx, 0, sizeof(g_ctx)); g_ctx.javaVM = vm; @@ -24,9 +25,7 @@ JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved) { return JNI_VERSION_1_6; } -static void -update_hook_callback(void *pData, int opCode, char const *pDbName, char const *pTableName, - sqlite3_int64 iRow) { +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; @@ -41,8 +40,7 @@ update_hook_callback(void *pData, int opCode, char const *pDbName, char const *p } } -static jint -commit_hook(void *pool) { +static int commit_hook(void *pool) { // Get JNIEnv for the current thread JNIEnv *env; JavaVM *javaVM = g_ctx.javaVM; @@ -72,8 +70,8 @@ static void rollback_hook(void *pool) { } } -jint powersync_init(sqlite3 *db, char **pzErrMsg, - const sqlite3_api_routines *pApi) { +int powersync_init(sqlite3 *db, char **pzErrMsg, const sqlite3_api_routines *pApi) { + sqlite3_initialize(); sqlite3_update_hook(db, update_hook_callback, NULL); sqlite3_commit_hook(db, commit_hook, NULL); @@ -82,10 +80,11 @@ jint powersync_init(sqlite3 *db, char **pzErrMsg, return SQLITE_OK; } -JNIEXPORT void JNICALL -Java_com_powersync_DatabaseDriverFactory_setupSqliteBinding(JNIEnv *env, jobject thiz) { +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); } + } diff --git a/demos/hello-powersync/.gitignore b/demos/hello-powersync/.gitignore index 33e4c75d..10539847 100644 --- a/demos/hello-powersync/.gitignore +++ b/demos/hello-powersync/.gitignore @@ -15,3 +15,4 @@ captures !*.xcodeproj/project.xcworkspace/ !*.xcworkspace/contents.xcworkspacedata **/xcshareddata/WorkspaceSettings.xcsettings +**/powersync.db diff --git a/demos/hello-powersync/README.md b/demos/hello-powersync/README.md index 2896b3cc..00a4c278 100644 --- a/demos/hello-powersync/README.md +++ b/demos/hello-powersync/README.md @@ -85,9 +85,14 @@ SUPABASE_USER_PASSWORD=foo ``` ## Run the App -Choose a run configuration for the Android (`composeApp`) or iOS (`iosApp`) target in Android Studio and run it. + +- To run the Android app, run the `composeApp` Android Studio target configuration. +- To run the iOS app, run the `iosApp` Android Studio target configuration. ![run-configuration](/docs/assets/android-studio-run-configuration.png) + +- To run the desktop app, run the `run` Gradle task (in the "compose desktop" group) or execute `./gradlew run`. + ## Project structure @@ -98,7 +103,6 @@ Choose a run configuration for the Android (`composeApp`) or iOS (`iosApp`) targ - `commonMain` is for code that’s common for all targets. - Other folders are for Kotlin code that will be compiled for only the platform indicated in the folder name. - `iosMain` would be the right folder for such calls. * `/iosApp` contains iOS applications. Even if you’re sharing your UI with Compose Multiplatform, you need this entry point for your iOS app. This is also where you should add SwiftUI code for diff --git a/demos/hello-powersync/composeApp/build.gradle.kts b/demos/hello-powersync/composeApp/build.gradle.kts index de9641cc..6cc4585a 100644 --- a/demos/hello-powersync/composeApp/build.gradle.kts +++ b/demos/hello-powersync/composeApp/build.gradle.kts @@ -15,6 +15,8 @@ plugins { kotlin { androidTarget() + jvm() + // iosX64() // uncomment to enable iOS x64 iosArm64() iosSimulatorArm64() @@ -55,6 +57,10 @@ kotlin { implementation(projectLibs.compose.ui.tooling.preview) implementation(projectLibs.androidx.activity.compose) } + + jvmMain.dependencies { + implementation(compose.desktop.currentOs) + } } } @@ -135,3 +141,7 @@ buildkonfig { stringConfigField("SUPABASE_USER_PASSWORD") } } + +compose.desktop.application { + mainClass = "com.powersync.demos.MainKt" +} diff --git a/demos/hello-powersync/composeApp/src/jvmMain/kotlin/com/powersync/demos/main.kt b/demos/hello-powersync/composeApp/src/jvmMain/kotlin/com/powersync/demos/main.kt new file mode 100644 index 00000000..faa6f45d --- /dev/null +++ b/demos/hello-powersync/composeApp/src/jvmMain/kotlin/com/powersync/demos/main.kt @@ -0,0 +1,8 @@ +package com.powersync.demos + +import androidx.compose.ui.window.singleWindowApplication + + +fun main() = singleWindowApplication { + App() +} diff --git a/demos/hello-powersync/iosApp/Podfile.lock b/demos/hello-powersync/iosApp/Podfile.lock index b6b7c134..9ee6106f 100644 --- a/demos/hello-powersync/iosApp/Podfile.lock +++ b/demos/hello-powersync/iosApp/Podfile.lock @@ -20,4 +20,4 @@ SPEC CHECKSUMS: PODFILE CHECKSUM: 4680f51fbb293d1385fb2467ada435cc1f16ab3d -COCOAPODS: 1.15.2 +COCOAPODS: 1.16.2 diff --git a/demos/hello-powersync/iosApp/iosApp.xcodeproj/project.pbxproj b/demos/hello-powersync/iosApp/iosApp.xcodeproj/project.pbxproj index 9781da42..9481617e 100644 --- a/demos/hello-powersync/iosApp/iosApp.xcodeproj/project.pbxproj +++ b/demos/hello-powersync/iosApp/iosApp.xcodeproj/project.pbxproj @@ -106,7 +106,6 @@ 7555FF79242A565900829871 /* Resources */, EF8A45596C10845FBC80C40F /* Frameworks */, BDEF1F2128374088F98D10FB /* [CP] Embed Pods Frameworks */, - 35538324538A9B616ADCBF80 /* [CP] Copy Pods Resources */, ); buildRules = ( ); @@ -163,23 +162,6 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ - 35538324538A9B616ADCBF80 /* [CP] Copy Pods Resources */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-iosApp/Pods-iosApp-resources-${CONFIGURATION}-input-files.xcfilelist", - ); - name = "[CP] Copy Pods Resources"; - outputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-iosApp/Pods-iosApp-resources-${CONFIGURATION}-output-files.xcfilelist", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-iosApp/Pods-iosApp-resources.sh\"\n"; - showEnvVarsInLog = 0; - }; BDEF1F2128374088F98D10FB /* [CP] Embed Pods Frameworks */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; diff --git a/demos/hello-powersync/settings.gradle.kts b/demos/hello-powersync/settings.gradle.kts index 46c9a39d..271061d1 100644 --- a/demos/hello-powersync/settings.gradle.kts +++ b/demos/hello-powersync/settings.gradle.kts @@ -10,6 +10,7 @@ pluginManagement { } dependencyResolutionManagement { + @Suppress("UnstableApiUsage") repositories { google() mavenCentral() diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 86c48d74..bba9df57 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -15,8 +15,9 @@ kotlinx-datetime = "0.5.0" kotlinx-io = "0.5.4" ktor = "3.0.1" uuid = "0.8.2" -powersync-core = "0.3.1" +powersync-core = "0.3.6" sqlite-android = "3.45.0" +sqlite-jdbc = "3.45.2.0" sqlDelight = "2.0.2" stately = "2.0.7" @@ -49,7 +50,7 @@ androidx-test-junit = "1.2.1" configuration-annotations = { module = "co.touchlab.skie:configuration-annotations", version.ref = "configurationAnnotations" } kermit = { module = "co.touchlab:kermit", version.ref = "kermit" } kermit-test = { module = "co.touchlab:kermit-test", version.ref = "kermit" } -powersync-sqlite-core = { module = "co.powersync:powersync-sqlite-core", version.ref = "powersync-core" } +powersync-sqlite-core-android = { module = "co.powersync:powersync-sqlite-core", version.ref = "powersync-core" } mavenPublishPlugin = { module = "com.vanniktech:gradle-maven-publish-plugin", version.ref = "maven-publish" } test-junit = { group = "junit", name = "junit", version.ref = "junit" } @@ -73,6 +74,7 @@ kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-c sqldelight-driver-ios = { module = "app.cash.sqldelight:native-driver", version.ref = "sqlDelight" } sqldelight-driver-android = { module = "app.cash.sqldelight:android-driver", version.ref = "sqlDelight" } +sqldelight-driver-jdbc = { module = "app.cash.sqldelight:sqlite-driver", version.ref = "sqlDelight" } requery-sqlite-android = { module = "com.github.requery:sqlite-android", version.ref = "sqlite-android" } sqldelight-coroutines = { module = "app.cash.sqldelight:coroutines-extensions", version.ref = "sqlDelight" } sqldelight-runtime = { module = "app.cash.sqldelight:runtime", version.ref = "sqlDelight" } @@ -80,6 +82,8 @@ sqldelight-dialect-sqlite338 = { module = "app.cash.sqldelight:sqlite-3-38-diale sqldelight-dialect-sqlite335 = { module = "app.cash.sqldelight:sqlite-3-35-dialect", version.ref = "sqlDelight" } sqldelight-compilerEnv = { module = "app.cash.sqldelight:compiler-env", version.ref = "sqlDelight" } +sqlite-jdbc = { module = "org.xerial:sqlite-jdbc", version.ref = "sqlite-jdbc" } + stately-concurrency = { module = "co.touchlab:stately-concurrency", version.ref = "stately" } supabase-client = { module = "io.github.jan-tennert.supabase:postgrest-kt", version.ref = "supabase" } supabase-auth = { module = "io.github.jan-tennert.supabase:auth-kt", version.ref = "supabase" } diff --git a/persistence/build.gradle.kts b/persistence/build.gradle.kts index 977c9e7c..75b1bdfa 100644 --- a/persistence/build.gradle.kts +++ b/persistence/build.gradle.kts @@ -13,6 +13,8 @@ kotlin { publishLibraryVariants("release", "debug") } + jvm() + iosX64() iosArm64() iosSimulatorArm64() @@ -27,10 +29,14 @@ kotlin { androidMain.dependencies { api(libs.sqldelight.driver.android) - api(libs.powersync.sqlite.core) + api(libs.powersync.sqlite.core.android) api(libs.requery.sqlite.android) } + jvmMain.dependencies { + api(libs.sqldelight.driver.jdbc) + } + iosMain.dependencies { api(libs.sqldelight.driver.ios) } From 5cccad1b0854c9f8814294ed8843faf7cf94ad36 Mon Sep 17 00:00:00 2001 From: Salomon BRYS Date: Tue, 24 Dec 2024 12:40:07 +0100 Subject: [PATCH 2/3] Allow to manually set CMake path in local.properties. --- core/build.gradle.kts | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/core/build.gradle.kts b/core/build.gradle.kts index 37fb657b..fd420442 100644 --- a/core/build.gradle.kts +++ b/core/build.gradle.kts @@ -3,6 +3,7 @@ import com.powersync.plugins.sonatype.setupGithubRepository import de.undercouch.gradle.tasks.download.Download import org.gradle.internal.os.OperatingSystem import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget +import java.util.* plugins { alias(libs.plugins.kotlinMultiplatform) @@ -223,6 +224,13 @@ val getBinaries = if (binariesAreProvided) { } 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, @@ -237,7 +245,7 @@ val getBinaries = if (binariesAreProvided) { sqliteSrcFolder, ) outputs.dir(workingDir) - executable = "cmake" + executable = cmakeExecutable args(listOf(file("src/jvmMain/cpp/CMakeLists.txt").absolutePath, "-DSUFFIX=$suffix", "-DCMAKE_BUILD_TYPE=Release") + defines.map { "-D$it" }) doFirst { workingDir.mkdirs() @@ -255,7 +263,7 @@ val getBinaries = if (binariesAreProvided) { workingDir, ) outputs.dir(workingDir.resolve(if (os.isWindows) "output/Release" else "output")) - executable = "cmake" + executable = cmakeExecutable args("--build", ".", "--config", "Release") } From 85947057577a9a060e391f827660a7252c934398 Mon Sep 17 00:00:00 2001 From: Salomon BRYS Date: Wed, 25 Dec 2024 12:07:41 +0100 Subject: [PATCH 3/3] Only builds system arch by default & introduces the powersync.binaries.cross-arch to cross-compile architecture binaries. --- .github/workflows/deploy.yml | 3 +- core/build.gradle.kts | 83 +++++++++++++++++++++--------------- 2 files changed, 51 insertions(+), 35 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index cc90025d..24151ff0 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -33,6 +33,7 @@ jobs: run: | ./gradlew \ -PGITHUB_PUBLISH_TOKEN=${{ secrets.GITHUB_TOKEN }} \ + -Ppowersync.binaries.cross-arch=true \ :core:cmakeJvmBuild - name: Upload build uses: actions/upload-artifact@v4 @@ -74,7 +75,7 @@ jobs: -PsigningInMemoryKeyPassword="${{ secrets.SIGNING_PASSWORD }}" \ -PcentralPortal.username="${{secrets.SONATYPE_USERNAME}}" \ -PcentralPortal.password="${{secrets.SONATYPE_PASSWORD}}" \ - -Ppowersync.binaries.provided="true" + -Ppowersync.binaries.provided="true" \ publishAllPublicationsToSonatypeRepository # This will change Package.swift in Github packages to direct to new maven central KMMBridge zip file call-kmmbridge-publish: diff --git a/core/build.gradle.kts b/core/build.gradle.kts index fd420442..9f4dd8ed 100644 --- a/core/build.gradle.kts +++ b/core/build.gradle.kts @@ -196,9 +196,12 @@ android { val os = OperatingSystem.current() val binariesAreProvided = project.findProperty("powersync.binaries.provided") == "true" -val binariesFolder = - project.layout.buildDirectory - .dir("binaries/desktop") +val crossArch = project.findProperty("powersync.binaries.cross-arch") == "true" +val binariesFolder = project.layout.buildDirectory.dir("binaries/desktop") + +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 @@ -270,22 +273,32 @@ val getBinaries = if (binariesAreProvided) { return cmakeBuild } - val cmakeJvmBuilds = when { - os.isMacOsX -> listOf( - registerCMakeTasks("aarch64", "CMAKE_OSX_ARCHITECTURES=arm64"), - registerCMakeTasks("x64", "CMAKE_OSX_ARCHITECTURES=x86_64"), - ) - os.isLinux -> listOf( - registerCMakeTasks("aarch64", "CMAKE_C_COMPILER=aarch64-linux-gnu-gcc", "CMAKE_CXX_COMPILER=aarch64-linux-gnu-g++"), - registerCMakeTasks( - "x64", "CMAKE_C_COMPILER=x86_64-linux-gnu-gcc", "CMAKE_CXX_COMPILER=x86_64-linux-gnu-g++"), - ) - os.isWindows -> listOf( - registerCMakeTasks("x64"), - ) + 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") } + val arch = System.getProperty("os.arch") + val cmakeJvmBuilds = when { + crossArch -> listOfNotNull(aarch64, x64) + arch == "aarch64" -> listOfNotNull(aarch64) + arch == "amd64" -> listOfNotNull(x64) + else -> error("Unknown architecture: $arch") + } + tasks.register("cmakeJvmBuild") { dependsOn(cmakeJvmBuilds) group = "cmake" @@ -296,26 +309,28 @@ val getBinaries = if (binariesAreProvided) { val downloadPowersyncDesktopBinaries = tasks.register("downloadPowersyncDesktopBinaries") { val coreVersion = libs.versions.powersync.core.get() - val linux = listOf( - "https://github.com/powersync-ja/powersync-sqlite-core/releases/download/v$coreVersion/libpowersync_aarch64.so", - "https://github.com/powersync-ja/powersync-sqlite-core/releases/download/v$coreVersion/libpowersync_x64.so", - ) - val mac = listOf( - "https://github.com/powersync-ja/powersync-sqlite-core/releases/download/v$coreVersion/libpowersync_aarch64.dylib", - "https://github.com/powersync-ja/powersync-sqlite-core/releases/download/v$coreVersion/libpowersync_x64.dylib", - ) - val windows = listOf( - "https://github.com/powersync-ja/powersync-sqlite-core/releases/download/v$coreVersion/powersync_x64.dll", - ) - src(when { - binariesAreProvided -> linux + mac + windows - else -> when { - os.isLinux -> linux - os.isMacOsX -> mac - os.isWindows -> windows + 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" -> listOfNotNull(x64) + else -> error("Unknown architecture: $arch") + }) + } dest(binariesFolder.map { it.dir("powersync") }) onlyIfModified(true) }