Skip to content

Commit 15e1039

Browse files
authored
Encryption part 1: Restructure SDK modules (#276)
* Start moving code into common * Share common logic in test * Well, compiling works * Fixing some tests * Update readmes * Reformat * Fix supabase test * Fix watchos tests * Fix example * Add changelog entries * Be more explicit about potential breakage * Review feedback
1 parent ee9984a commit 15e1039

File tree

156 files changed

+1020
-733
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

156 files changed

+1020
-733
lines changed

CHANGELOG.md

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,14 @@
11
# Changelog
22

3-
## 1.7.0 (unreleased)
3+
## 1.8.0 (unreleased)
4+
5+
- Refactor SDK: `com.powersync:powersync-core` has an identical API, but now depends on
6+
`com.powersync:powersync-common` where most logic is implemented.
7+
- __POTENTIALLY BREAKING CHANGE__: If you were injecting a `DatabaseDriverFactory` into Koin or Dagger, note that the
8+
`PowerSyncDatabase()` factory method now takes a more generic `PersistentConnectionFactory`.
9+
- If you're using `PowerSyncDatabase.inMemory`, you explicitly have to import `com.powersync.inMemory` now.
10+
11+
## 1.7.0
412

513
- Add `PowerSyncDatabase.inMemory` to create an in-memory SQLite database with PowerSync.
614
This may be useful for testing.

README.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,14 @@ and API documentation [here](https://powersync-ja.github.io/powersync-kotlin/).
2424

2525
- [core](./core/)
2626

27-
- This is the Kotlin Multiplatform SDK implementation.
27+
- This is the Kotlin Multiplatform SDK implementation, built by depending on `common`
28+
and linking SQLite.
29+
30+
- [common](./common/)
31+
32+
- This is the Kotlin Multiplatform SDK implementation without a dependency on a fixed
33+
SQLite bundle. This allows the SDK to be used with custom SQLite installations (like
34+
e.g. SQLCipher).
2835

2936
- [integrations](./integrations/)
3037
- [room](./integrations/room/README.md): Allows using the [Room database library](https://developer.android.com/jetpack/androidx/releases/room) with PowerSync, making it easier to run typed queries on the database.

common/README.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# PowerSync common
2+
3+
This module contains core definitions for the PowerSync SDK, without linking or bundling a SQLite dependency.
4+
5+
This allows the module to be used as a building block for PowerSync SDKs with and without encryption support.
6+
7+
Users should typically depend on `:core` instead.
8+
9+
## Structure
10+
11+
This is a Kotlin Multiplatform project targeting Android, iOS platforms, with the following
12+
structure:
13+
14+
- `commonMain` - Shared code for all targets, which includes the `PowerSyncBackendConnector`
15+
interface and `PowerSyncBuilder` for building a `PowerSync` instance. It also defines
16+
the `DatabaseDriverFactory` class to be implemented in each platform.
17+
- `commonJava` - Shared logic for Android and Java targets.
18+
- `androidMain` - Android-specific code for loading the core extension.
19+
- `jvmMain` - Java-specific code for loading the core extension.
20+
- `nativeMain` - A SQLite driver implemented with cinterop calls to sqlite3.
21+
- `appleMain`: Utilities for finding a suitable database location on Apple platforms.
22+
- `appleNonWatchOsMain` and `watchosMain`: Loads the PowerSync core extension (which is linked statically on watchOS
23+
and dynamically on other platforms).
24+
25+
## Attachment Helpers
26+
27+
This module contains attachment helpers under the `com.powersync.attachments` package. See
28+
the [Attachment Helpers README](../common/src/commonMain/kotlin/com/powersync/attachments/README.md)

common/build.gradle.kts

Lines changed: 277 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,277 @@
1+
import com.powersync.plugins.utils.powersyncTargets
2+
import de.undercouch.gradle.tasks.download.Download
3+
import org.gradle.api.tasks.testing.logging.TestExceptionFormat
4+
import org.gradle.internal.os.OperatingSystem
5+
import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget
6+
import org.jetbrains.kotlin.gradle.targets.jvm.tasks.KotlinJvmTest
7+
import org.jetbrains.kotlin.gradle.tasks.KotlinTest
8+
import org.jetbrains.kotlin.konan.target.Family
9+
import java.nio.file.Path
10+
import kotlin.io.path.createDirectories
11+
import kotlin.io.path.writeText
12+
13+
plugins {
14+
alias(libs.plugins.kotlinMultiplatform)
15+
alias(libs.plugins.kotlinSerialization)
16+
alias(libs.plugins.android.library)
17+
alias(libs.plugins.mavenPublishPlugin)
18+
alias(libs.plugins.downloadPlugin)
19+
alias(libs.plugins.kotlinter)
20+
id("com.powersync.plugins.sonatype")
21+
id("com.powersync.plugins.sharedbuild")
22+
alias(libs.plugins.mokkery)
23+
alias(libs.plugins.kotlin.atomicfu)
24+
id("dokka-convention")
25+
}
26+
27+
val binariesFolder = project.layout.buildDirectory.dir("binaries/desktop")
28+
val downloadPowersyncDesktopBinaries by tasks.registering(Download::class) {
29+
description = "Download PowerSync core extensions for JVM builds and releases"
30+
31+
val coreVersion =
32+
libs.versions.powersync.core
33+
.get()
34+
val linux_aarch64 =
35+
"https://github.com/powersync-ja/powersync-sqlite-core/releases/download/v$coreVersion/libpowersync_aarch64.so"
36+
val linux_x64 =
37+
"https://github.com/powersync-ja/powersync-sqlite-core/releases/download/v$coreVersion/libpowersync_x64.so"
38+
val macos_aarch64 =
39+
"https://github.com/powersync-ja/powersync-sqlite-core/releases/download/v$coreVersion/libpowersync_aarch64.dylib"
40+
val macos_x64 =
41+
"https://github.com/powersync-ja/powersync-sqlite-core/releases/download/v$coreVersion/libpowersync_x64.dylib"
42+
val windows_x64 =
43+
"https://github.com/powersync-ja/powersync-sqlite-core/releases/download/v$coreVersion/powersync_x64.dll"
44+
45+
val includeAllPlatformsForJvmBuild =
46+
project.findProperty("powersync.binaries.allPlatforms") == "true"
47+
val os = OperatingSystem.current()
48+
49+
// The jar we're releasing for JVM clients needs to include the core extension. For local tests, it's enough to only
50+
// download the extension for the OS running the build. For releases, we want to include them all.
51+
// We're not compiling native code for JVM builds here (we're doing that for Android only), so we just have to
52+
// fetch prebuilt binaries from the powersync-sqlite-core repository.
53+
if (includeAllPlatformsForJvmBuild) {
54+
src(listOf(linux_aarch64, linux_x64, macos_aarch64, macos_x64, windows_x64))
55+
} else {
56+
val (aarch64, x64) =
57+
when {
58+
os.isLinux -> linux_aarch64 to linux_x64
59+
os.isMacOsX -> macos_aarch64 to macos_x64
60+
os.isWindows -> null to windows_x64
61+
else -> error("Unknown operating system: $os")
62+
}
63+
val arch = System.getProperty("os.arch")
64+
src(
65+
when (arch) {
66+
"aarch64" -> listOfNotNull(aarch64)
67+
"amd64", "x86_64" -> listOfNotNull(x64)
68+
else -> error("Unsupported architecture: $arch")
69+
},
70+
)
71+
}
72+
dest(binariesFolder.map { it.dir("powersync") })
73+
onlyIfModified(true)
74+
}
75+
76+
val generateVersionConstant by tasks.registering {
77+
val target = project.layout.buildDirectory.dir("generated/constants")
78+
val packageName = "com.powersync.build"
79+
80+
outputs.dir(target)
81+
val currentVersion = version.toString()
82+
83+
doLast {
84+
val dir = target.get().asFile
85+
dir.mkdir()
86+
val rootPath = dir.toPath()
87+
88+
val source =
89+
"""
90+
package $packageName
91+
92+
internal const val LIBRARY_VERSION: String = "$currentVersion"
93+
94+
""".trimIndent()
95+
96+
val packageRoot = packageName.split('.').fold(rootPath, Path::resolve)
97+
packageRoot.createDirectories()
98+
99+
packageRoot.resolve("BuildConstants.kt").writeText(source)
100+
}
101+
}
102+
103+
kotlin {
104+
powersyncTargets()
105+
106+
targets.withType<KotlinNativeTarget> {
107+
compilations.named("main") {
108+
compileTaskProvider {
109+
compilerOptions.freeCompilerArgs.add("-Xexport-kdoc")
110+
}
111+
112+
if (target.konanTarget.family == Family.WATCHOS) {
113+
// We're linking the core extension statically, which means that we need a cinterop
114+
// to call powersync_init_static
115+
cinterops.create("powersync_static") {
116+
packageName("com.powersync.static")
117+
headers(file("src/watchosMain/powersync_static.h"))
118+
}
119+
}
120+
121+
cinterops.create("sqlite3") {
122+
// We're not linking SQLite here (to allow using this package with e.g. SQLCipher or
123+
// SQLite3MultipleCiphers), :core depends on this package and is responsible for bundling SQLite.
124+
packageName("com.powersync.internal.sqlite3")
125+
includeDirs.allHeaders("src/nativeMain/interop/")
126+
definitionFile.set(project.file("src/nativeMain/interop/sqlite3.def"))
127+
}
128+
}
129+
}
130+
131+
explicitApi()
132+
133+
applyDefaultHierarchyTemplate()
134+
sourceSets {
135+
all {
136+
languageSettings {
137+
optIn("kotlinx.cinterop.ExperimentalForeignApi")
138+
optIn("kotlin.time.ExperimentalTime")
139+
optIn("kotlin.experimental.ExperimentalObjCRefinement")
140+
}
141+
}
142+
143+
val commonIntegrationTest by creating {
144+
dependsOn(commonTest.get())
145+
}
146+
147+
val commonJava by creating {
148+
dependsOn(commonMain.get())
149+
}
150+
151+
commonMain.configure {
152+
kotlin {
153+
srcDir(generateVersionConstant)
154+
}
155+
156+
dependencies {
157+
api(libs.androidx.sqlite.sqlite)
158+
159+
implementation(libs.uuid)
160+
implementation(libs.kotlin.stdlib)
161+
implementation(libs.ktor.client.contentnegotiation)
162+
implementation(libs.ktor.serialization.json)
163+
implementation(libs.kotlinx.io)
164+
implementation(libs.kotlinx.coroutines.core)
165+
implementation(libs.kotlinx.datetime)
166+
implementation(libs.stately.concurrency)
167+
implementation(libs.configuration.annotations)
168+
api(libs.ktor.client.core)
169+
api(libs.kermit)
170+
}
171+
}
172+
173+
androidMain {
174+
dependsOn(commonJava)
175+
dependencies {
176+
api(libs.powersync.sqlite.core.android)
177+
}
178+
}
179+
180+
jvmMain {
181+
dependsOn(commonJava)
182+
}
183+
184+
// Common apple targets where we link the core extension dynamically
185+
val appleNonWatchOsMain by creating {
186+
dependsOn(appleMain.get())
187+
}
188+
189+
macosMain.orNull?.dependsOn(appleNonWatchOsMain)
190+
iosMain.orNull?.dependsOn(appleNonWatchOsMain)
191+
tvosMain.orNull?.dependsOn(appleNonWatchOsMain)
192+
193+
commonTest.dependencies {
194+
implementation(projects.internal.testutils)
195+
implementation(libs.kotlin.test)
196+
}
197+
198+
// We're putting the native libraries into our JAR, so integration tests for the JVM can run as part of the unit
199+
// tests.
200+
jvmTest {
201+
dependsOn(commonIntegrationTest)
202+
}
203+
204+
// We have special setup in this build configuration to make these tests link the PowerSync extension, so they
205+
// can run integration tests along with the executable for unit testing.
206+
appleTest.orNull?.dependsOn(commonIntegrationTest)
207+
}
208+
}
209+
210+
android {
211+
compileOptions {
212+
targetCompatibility = JavaVersion.VERSION_17
213+
}
214+
215+
buildFeatures {
216+
buildConfig = true
217+
}
218+
219+
buildTypes {
220+
release {
221+
buildConfigField("boolean", "DEBUG", "false")
222+
}
223+
debug {
224+
buildConfigField("boolean", "DEBUG", "true")
225+
}
226+
}
227+
228+
namespace = "com.powersync"
229+
compileSdk =
230+
libs.versions.android.compileSdk
231+
.get()
232+
.toInt()
233+
defaultConfig {
234+
minSdk =
235+
libs.versions.android.minSdk
236+
.get()
237+
.toInt()
238+
consumerProguardFiles("proguard-rules.pro")
239+
}
240+
241+
ndkVersion = "27.1.12297006"
242+
}
243+
244+
tasks.named<ProcessResources>(kotlin.jvm().compilations["main"].processResourcesTaskName) {
245+
from(downloadPowersyncDesktopBinaries)
246+
}
247+
248+
// We want to build with recent JDKs, but need to make sure we support Java 8. https://jakewharton.com/build-on-latest-java-test-through-lowest-java/
249+
val testWithJava8 by tasks.registering(KotlinJvmTest::class) {
250+
javaLauncher =
251+
javaToolchains.launcherFor {
252+
languageVersion = JavaLanguageVersion.of(8)
253+
}
254+
255+
description = "Run tests with Java 8"
256+
group = LifecycleBasePlugin.VERIFICATION_GROUP
257+
258+
// Copy inputs from the normal test task
259+
val testTask = tasks.getByName("jvmTest") as KotlinJvmTest
260+
classpath = testTask.classpath
261+
testClassesDirs = testTask.testClassesDirs
262+
}
263+
tasks.named("check").configure { dependsOn(testWithJava8) }
264+
265+
tasks.withType<KotlinTest> {
266+
testLogging {
267+
events("PASSED", "FAILED", "SKIPPED")
268+
exceptionFormat = TestExceptionFormat.FULL
269+
showCauses = true
270+
showStandardStreams = true
271+
showStackTraces = true
272+
}
273+
}
274+
275+
dokka {
276+
moduleName.set("PowerSync Common")
277+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
package com.powersync
2+
3+
@ExperimentalPowerSyncAPI
4+
@Throws(PowerSyncException::class)
5+
public actual fun resolvePowerSyncLoadableExtensionPath(): String? = "libpowersync.so"

core/src/appleMain/kotlin/com/powersync/DatabaseDriverFactory.apple.kt renamed to common/src/appleMain/kotlin/com/powersync/PathUtils.kt

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,18 @@
11
package com.powersync
22

33
import kotlinx.cinterop.UnsafeNumber
4-
import kotlinx.io.files.FileSystem
54
import platform.Foundation.NSApplicationSupportDirectory
65
import platform.Foundation.NSBundle
76
import platform.Foundation.NSFileManager
87
import platform.Foundation.NSSearchPathForDirectoriesInDomains
98
import platform.Foundation.NSUserDomainMask
109
import kotlin.getValue
1110

11+
/**
12+
* The default path to use for databases on Apple platforms.
13+
*/
1214
@OptIn(UnsafeNumber::class)
13-
internal fun appleDefaultDatabasePath(dbFilename: String): String {
15+
public fun appleDefaultDatabasePath(dbFilename: String): String {
1416
// This needs to be compatible with https://github.com/touchlab/SQLiter/blob/a37bbe7e9c65e6a5a94c5bfcaccdaae55ad2bac9/sqliter-driver/src/appleMain/kotlin/co/touchlab/sqliter/DatabaseFileContext.kt#L36-L51
1517
val paths = NSSearchPathForDirectoriesInDomains(NSApplicationSupportDirectory, NSUserDomainMask, true)
1618
val documentsDirectory = paths[0] as String
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
package com.powersync
2+
3+
@ExperimentalPowerSyncAPI
4+
@Throws(PowerSyncException::class)
5+
public actual fun resolvePowerSyncLoadableExtensionPath(): String? = powerSyncExtensionPath

0 commit comments

Comments
 (0)