diff --git a/.changeset/tough-eggs-behave.md b/.changeset/tough-eggs-behave.md
new file mode 100644
index 000000000..a2dec4b95
--- /dev/null
+++ b/.changeset/tough-eggs-behave.md
@@ -0,0 +1,7 @@
+---
+'@powersync/react-native': patch
+'@powersync/capacitor': patch
+'@powersync/node': patch
+---
+
+Android: Bundle PowerSync SQLite core extension with package instead of downloading it as a Gradle dependency.
diff --git a/demos/example-capacitor/android/app/build.gradle b/demos/example-capacitor/android/app/build.gradle
index e8fe1b240..42d78ee59 100644
--- a/demos/example-capacitor/android/app/build.gradle
+++ b/demos/example-capacitor/android/app/build.gradle
@@ -19,7 +19,7 @@ android {
buildTypes {
release {
minifyEnabled false
- proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+ proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
}
diff --git a/demos/example-capacitor/android/build.gradle b/demos/example-capacitor/android/build.gradle
index f8f0e43b6..d2669f381 100644
--- a/demos/example-capacitor/android/build.gradle
+++ b/demos/example-capacitor/android/build.gradle
@@ -7,7 +7,7 @@ buildscript {
mavenCentral()
}
dependencies {
- classpath 'com.android.tools.build:gradle:8.13.0'
+ classpath 'com.android.tools.build:gradle:9.3.2'
classpath 'com.google.gms:google-services:4.4.4'
// NOTE: Do not place your application dependencies here; they belong
diff --git a/demos/example-capacitor/android/capacitor.settings.gradle b/demos/example-capacitor/android/capacitor.settings.gradle
index 54d087ba6..919548cfa 100644
--- a/demos/example-capacitor/android/capacitor.settings.gradle
+++ b/demos/example-capacitor/android/capacitor.settings.gradle
@@ -1,12 +1,12 @@
// DO NOT EDIT THIS FILE! IT IS GENERATED EACH TIME "capacitor update" IS RUN
include ':capacitor-android'
-project(':capacitor-android').projectDir = new File('../node_modules/.pnpm/@capacitor+android@8.3.4_@capacitor+core@8.3.4/node_modules/@capacitor/android/capacitor')
+project(':capacitor-android').projectDir = new File('../node_modules/.pnpm/@capacitor+android@8.5.1_@capacitor+core@8.5.1/node_modules/@capacitor/android/capacitor')
include ':capacitor-community-sqlite'
-project(':capacitor-community-sqlite').projectDir = new File('../node_modules/.pnpm/@capacitor-community+sqlite@8.1.0_@capacitor+core@8.3.4/node_modules/@capacitor-community/sqlite/android')
+project(':capacitor-community-sqlite').projectDir = new File('../node_modules/.pnpm/@capacitor-community+sqlite@8.1.1_@capacitor+core@8.5.1/node_modules/@capacitor-community/sqlite/android')
include ':capacitor-splash-screen'
-project(':capacitor-splash-screen').projectDir = new File('../node_modules/.pnpm/@capacitor+splash-screen@8.0.1_@capacitor+core@8.3.4/node_modules/@capacitor/splash-screen/android')
+project(':capacitor-splash-screen').projectDir = new File('../node_modules/.pnpm/@capacitor+splash-screen@8.0.2_@capacitor+core@8.5.1/node_modules/@capacitor/splash-screen/android')
include ':powersync-capacitor'
project(':powersync-capacitor').projectDir = new File('../node_modules/@powersync/capacitor/android')
diff --git a/demos/example-capacitor/android/gradle/wrapper/gradle-wrapper.properties b/demos/example-capacitor/android/gradle/wrapper/gradle-wrapper.properties
index ed4c299ad..c42672d95 100644
--- a/demos/example-capacitor/android/gradle/wrapper/gradle-wrapper.properties
+++ b/demos/example-capacitor/android/gradle/wrapper/gradle-wrapper.properties
@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-all.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-9.7.1-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
diff --git a/demos/example-capacitor/ios/App/CapApp-SPM/Package.swift b/demos/example-capacitor/ios/App/CapApp-SPM/Package.swift
index 85fcf61d4..9e69d6d45 100644
--- a/demos/example-capacitor/ios/App/CapApp-SPM/Package.swift
+++ b/demos/example-capacitor/ios/App/CapApp-SPM/Package.swift
@@ -11,9 +11,9 @@ let package = Package(
targets: ["CapApp-SPM"])
],
dependencies: [
- .package(url: "https://github.com/ionic-team/capacitor-swift-pm.git", exact: "8.3.4"),
- .package(name: "CapacitorCommunitySqlite", path: "../../../node_modules/.pnpm/@capacitor-community+sqlite@8.1.0_@capacitor+core@8.3.4/node_modules/@capacitor-community/sqlite"),
- .package(name: "CapacitorSplashScreen", path: "../../../node_modules/.pnpm/@capacitor+splash-screen@8.0.1_@capacitor+core@8.3.4/node_modules/@capacitor/splash-screen"),
+ .package(url: "https://github.com/ionic-team/capacitor-swift-pm.git", exact: "8.5.1"),
+ .package(name: "CapacitorCommunitySqlite", path: "../../../node_modules/.pnpm/@capacitor-community+sqlite@8.1.1_@capacitor+core@8.5.1/node_modules/@capacitor-community/sqlite"),
+ .package(name: "CapacitorSplashScreen", path: "../../../node_modules/.pnpm/@capacitor+splash-screen@8.0.2_@capacitor+core@8.5.1/node_modules/@capacitor/splash-screen"),
.package(name: "PowersyncCapacitor", path: "../../../node_modules/@powersync/capacitor")
],
targets: [
diff --git a/package.json b/package.json
index 30c473db6..29b0702bc 100644
--- a/package.json
+++ b/package.json
@@ -3,8 +3,7 @@
"version": "1.0.0",
"private": true,
"description": "monorepo for PowerSync JavaScript SDKs",
- "main": "index.js",
- "scripts": {
+ "scripts": {
"reset:repo": "tsc -b scripts && node scripts/dist/reset-repo.js",
"build:packages": "pnpm run --filter {./packages/**} -r build",
"build:packages:prod": "pnpm run --filter {./packages/**} -r build:prod",
diff --git a/packages/capacitor/.gitignore b/packages/capacitor/.gitignore
index df9f0c202..633ab76be 100644
--- a/packages/capacitor/.gitignore
+++ b/packages/capacitor/.gitignore
@@ -27,6 +27,7 @@ DerivedData/
# Built application files
*.apk
*.ap_
+android/core
# Files for the ART/Dalvik VM
*.dex
diff --git a/packages/capacitor/android/.gitignore b/packages/capacitor/android/.gitignore
index c591fdeb4..c73c14ab9 100644
--- a/packages/capacitor/android/.gitignore
+++ b/packages/capacitor/android/.gitignore
@@ -1,2 +1,2 @@
/build
-.cxx
\ No newline at end of file
+.cxx
diff --git a/packages/capacitor/android/build.gradle b/packages/capacitor/android/build.gradle
index e31e9a43d..eb00c317d 100644
--- a/packages/capacitor/android/build.gradle
+++ b/packages/capacitor/android/build.gradle
@@ -3,7 +3,6 @@ ext {
androidxAppCompatVersion = project.hasProperty('androidxAppCompatVersion') ? rootProject.ext.androidxAppCompatVersion : '1.7.1'
androidxJunitVersion = project.hasProperty('androidxJunitVersion') ? rootProject.ext.androidxJunitVersion : '1.3.0'
androidxEspressoCoreVersion = project.hasProperty('androidxEspressoCoreVersion') ? rootProject.ext.androidxEspressoCoreVersion : '3.7.0'
- powerSyncCoreVersion = project.hasProperty('powerSyncCoreVersion') ? rootProject.ext.powerSyncCoreVersion : '0.5.3'
}
buildscript {
@@ -33,10 +32,16 @@ android {
prefab true
}
+ sourceSets {
+ main {
+ jniLibs.srcDirs += ["core/jniLibs"]
+ }
+ }
+
buildTypes {
release {
minifyEnabled false
- proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+ proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
lintOptions {
@@ -55,11 +60,6 @@ repositories {
dependencies {
- implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation project(':capacitor-android')
implementation "androidx.appcompat:appcompat:$androidxAppCompatVersion"
- implementation "com.powersync:powersync-sqlite-core:$powerSyncCoreVersion"
- testImplementation "junit:junit:$junitVersion"
- androidTestImplementation "androidx.test.ext:junit:$androidxJunitVersion"
- androidTestImplementation "androidx.test.espresso:espresso-core:$androidxEspressoCoreVersion"
}
diff --git a/packages/capacitor/android/src/androidTest/java/com/getcapacitor/android/ExampleInstrumentedTest.java b/packages/capacitor/android/src/androidTest/java/com/getcapacitor/android/ExampleInstrumentedTest.java
deleted file mode 100644
index 58020e16c..000000000
--- a/packages/capacitor/android/src/androidTest/java/com/getcapacitor/android/ExampleInstrumentedTest.java
+++ /dev/null
@@ -1,26 +0,0 @@
-package com.getcapacitor.android;
-
-import static org.junit.Assert.*;
-
-import android.content.Context;
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.platform.app.InstrumentationRegistry;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-/**
- * Instrumented test, which will execute on an Android device.
- *
- * @see Testing documentation
- */
-@RunWith(AndroidJUnit4.class)
-public class ExampleInstrumentedTest {
-
- @Test
- public void useAppContext() throws Exception {
- // Context of the app under test.
- Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
-
- assertEquals("com.getcapacitor.android", appContext.getPackageName());
- }
-}
diff --git a/packages/capacitor/android/src/test/java/com/getcapacitor/ExampleUnitTest.java b/packages/capacitor/android/src/test/java/com/getcapacitor/ExampleUnitTest.java
deleted file mode 100644
index a0fed0cfb..000000000
--- a/packages/capacitor/android/src/test/java/com/getcapacitor/ExampleUnitTest.java
+++ /dev/null
@@ -1,18 +0,0 @@
-package com.getcapacitor;
-
-import static org.junit.Assert.*;
-
-import org.junit.Test;
-
-/**
- * Example local unit test, which will execute on the development machine (host).
- *
- * @see Testing documentation
- */
-public class ExampleUnitTest {
-
- @Test
- public void addition_isCorrect() throws Exception {
- assertEquals(4, 2 + 2);
- }
-}
diff --git a/packages/capacitor/package.json b/packages/capacitor/package.json
index d3d65569d..89d3971b9 100644
--- a/packages/capacitor/package.json
+++ b/packages/capacitor/package.json
@@ -13,6 +13,7 @@
"unpkg": "dist/plugin.js",
"files": [
"android/src/main/",
+ "android/core/",
"android/build.gradle",
"android/proguard-rules.pro",
"dist/",
@@ -51,7 +52,7 @@
"scripts": {
"verify": "pnpm verify:ios && pnpm verify:android && pnpm verify:web",
"verify:ios": "cd example-app && pnpm install && cd ios/App && xcodebuild -project App.xcodeproj -scheme App -destination 'platform=iOS Simulator,name=iPhone 16,OS=latest'",
- "verify:android": "cd android && ./gradlew clean build test && cd ..",
+ "verify:android": "pnpm prepare:core && cd android && ./gradlew clean build test && cd ..",
"verify:web": "pnpm build",
"lint": "pnpm eslint && pnpm prettier -- --check && pnpm swiftlint -- lint",
"fmt": "pnpm eslint -- --fix && pnpm prettier -- --write && pnpm swiftlint -- --fix --format",
@@ -59,7 +60,8 @@
"prettier": "prettier \"**/*.{css,html,ts,js,java}\" --plugin=prettier-plugin-java",
"swiftlint": "node-swiftlint",
"docgen": "docgen --api PowerSyncPlugin --output-readme README.md --output-json dist/docs.json",
- "build": "pnpm clean && pnpm docgen && tsc && rollup -c rollup.config.mjs",
+ "prepare:core": "node ../../tools/download-core.js capacitor",
+ "build": "pnpm clean && pnpm prepare:core && pnpm docgen && tsc && rollup -c rollup.config.mjs",
"build:prod": "pnpm build",
"clean": "rimraf ./dist",
"watch": "tsc --watch",
diff --git a/packages/node/download_core.js b/packages/node/download_core.js
deleted file mode 100644
index 3ee319b89..000000000
--- a/packages/node/download_core.js
+++ /dev/null
@@ -1,142 +0,0 @@
-import { createHash } from 'node:crypto';
-import * as fs from 'node:fs/promises';
-import * as path from 'node:path';
-import { Readable } from 'node:stream';
-import { finished } from 'node:stream/promises';
-
-// When changing this version, run node download_core.js update_hashes
-const version = '0.5.3';
-const versionHashes = {
- 'powersync_x64.dll': 'b3f62293f26d3ee309880d30e1e54819ac21b4d0b1ba9b9177f92e1dcbfdb0c7',
- 'powersync_x86.dll': 'd2403446c5b2d0550eb99cb195db01798ef78a740ba4f462cd0b887e0ce4bd8a',
- 'powersync_aarch64.dll': '19afce715bf63b4f590fb2c00d18c3681b818cd6eee5a35284bd3a804f090b20',
- 'libpowersync_x86.linux.so': 'dc9be35f8a5b5511be8f6b28f8c9d1e00e6ecdb72074ea8e67b826474810276f',
- 'libpowersync_x64.linux.so': 'a6de0c79151ad6243ca18633f0df10bed32b692095fdf0f01ebdac6eb01d2edf',
- 'libpowersync_aarch64.linux.so': 'f27b1cadce2210541903ba2f4e9652782c9e14c913cde4f67c463cec00d46d14',
- 'libpowersync_armv7.linux.so': '3f6ddf0838fbd34e0986e21de0db89b5a0462cdc1eaee95e42c9dcdc1f0c4cc5',
- 'libpowersync_riscv64gc.linux.so': '5eaa1458153ab562ca0047aa2af4aa3303ee7291eabd92baeb2e9ab2f07935ba',
- 'libpowersync_x64.macos.dylib': '1209f802bcd886a112bd0de9ca5d7497a62af045fc64b78e703bdd465ee40979',
- 'libpowersync_aarch64.macos.dylib': '4fa96a98d7edb64a188493beb277bddcb7e85b286e2533d685ecf49ff51c6807'
-};
-
-const assets = Object.keys(versionHashes);
-
-const hashStream = async (input) => {
- for await (const chunk of input.pipe(createHash('sha256')).setEncoding('hex')) {
- return chunk;
- }
-};
-
-const hashLocal = async (filePath) => {
- try {
- const handle = await fs.open(filePath, 'r');
- const input = handle.createReadStream();
-
- const result = await hashStream(input);
- await handle.close();
- return result;
- } catch {
- return null;
- }
-};
-
-const downloadAsset = async (asset) => {
- const destinationPath = path.resolve('lib', asset);
- const expectedHash = versionHashes[asset];
-
- // Check if file exists and has correct hash
- const currentHash = await hashLocal(destinationPath);
- if (currentHash == expectedHash) {
- console.debug(`${asset} is up-to-date, skipping download`);
- return;
- }
-
- const url = `https://github.com/powersync-ja/powersync-sqlite-core/releases/download/v${version}/${asset}`;
- console.log(`Downloading ${url}`);
- const response = await fetch(url);
- if (response.status != 200) {
- throw `Could not download ${url}`;
- }
-
- const file = await fs.open(destinationPath, 'w');
- await finished(Readable.fromWeb(response.body).pipe(file.createWriteStream()));
- await file.close();
-
- const hashAfterDownloading = await hashLocal(destinationPath);
- if (hashAfterDownloading != expectedHash) {
- throw `Unexpected hash after downloading ${asset} (got ${hashAfterDownloading}, expected ${expectedHash})`;
- }
- console.log(`Successfully downloaded ${asset}`);
-};
-
-const checkAsset = async (asset) => {
- const destinationPath = path.resolve('lib', asset);
- const expectedHash = versionHashes[asset];
- const currentHash = await hashLocal(destinationPath);
-
- return {
- asset,
- destinationPath,
- expectedHash,
- currentHash,
- exists: currentHash !== null,
- isValid: currentHash == expectedHash
- };
-};
-
-const download = async () => {
- try {
- await fs.access('lib');
- } catch {
- await fs.mkdir('lib');
- }
-
- // First check all assets
- console.log('Checking existing files...');
- const checks = await Promise.all(assets.map((asset) => checkAsset(asset, asset)));
-
- const toDownload = checks.filter((check) => !check.isValid);
- const upToDate = checks.filter((check) => check.isValid);
-
- // Print summary
- if (upToDate.length > 0) {
- console.log('\nUp-to-date files:');
- for (const check of upToDate) {
- console.log(` ✓ ${check.asset}`);
- }
- }
-
- if (toDownload.length > 0) {
- console.log('\nFiles to download:');
- for (const check of toDownload) {
- if (!check.exists) {
- console.log(` • ${check.asset} (missing)`);
- } else {
- console.log(` • ${check.asset} (hash mismatch)`);
- }
- }
-
- console.log('\nStarting downloads...');
- await Promise.all(toDownload.map((check) => downloadAsset(check.asset)));
-
- console.log('\nAll downloads completed successfully!');
- } else {
- console.log('\nAll files are up-to-date, nothing to download.');
- }
-};
-
-const updateReferenceHashes = async () => {
- for (const asset of Object.keys(versionHashes)) {
- const url = `https://github.com/powersync-ja/powersync-sqlite-core/releases/download/v${version}/${asset}`;
- const response = await fetch(url);
- const hash = await hashStream(Readable.fromWeb(response.body));
-
- console.log(` '${asset}': '${hash}',`);
- }
-};
-
-if (process.argv[process.argv.length - 1] == 'update_hashes') {
- await updateReferenceHashes();
-} else {
- await download();
-}
diff --git a/packages/node/package.json b/packages/node/package.json
index b626151bc..7c649689b 100644
--- a/packages/node/package.json
+++ b/packages/node/package.json
@@ -10,12 +10,11 @@
"files": [
"lib",
"dist",
- "download_core.js",
"src"
],
"scripts": {
- "prepare:core": "node download_core.js",
- "build": " pnpm prepare:core && tsc -b",
+ "prepare:core": "node ../../tools/download-core.js node",
+ "build": "pnpm prepare:core && tsc -b",
"build:prod": "pnpm prepare:core && tsc -b",
"clean": "rm -rf lib dist tsconfig.tsbuildinfo",
"watch": "tsc -b -w",
diff --git a/packages/react-native/.gitignore b/packages/react-native/.gitignore
new file mode 100644
index 000000000..8aa7a874a
--- /dev/null
+++ b/packages/react-native/.gitignore
@@ -0,0 +1 @@
+android/core/
diff --git a/packages/react-native/android/.gitignore b/packages/react-native/android/.gitignore
index f7ab597e6..9f2a07880 100644
--- a/packages/react-native/android/.gitignore
+++ b/packages/react-native/android/.gitignore
@@ -1,2 +1,2 @@
build/
-.gradle/
\ No newline at end of file
+.gradle/
diff --git a/packages/react-native/android/build.gradle b/packages/react-native/android/build.gradle
index 9acaa9c17..bdc114286 100644
--- a/packages/react-native/android/build.gradle
+++ b/packages/react-native/android/build.gradle
@@ -45,11 +45,15 @@ def supportsNamespace() {
android {
if (supportsNamespace()) {
namespace "com.powersync.opsqlite"
+ }
- sourceSets {
- main {
+ sourceSets {
+ main {
+ if (supportsNamespace()) {
manifest.srcFile "src/main/AndroidManifestNew.xml"
}
+
+ jniLibs.srcDirs += ["core/jniLibs"]
}
}
@@ -88,7 +92,6 @@ repositories {
def kotlin_version = getExtOrDefault("kotlinVersion")
dependencies {
- implementation 'com.powersync:powersync-sqlite-core:0.5.3'
// For < 0.71, this will be from the local maven repo
// For > 0.71, this will be replaced by `com.facebook.react:react-android:$version` by react gradle plugin
//noinspection GradleDynamicVersion
diff --git a/packages/react-native/package.json b/packages/react-native/package.json
index 4d20a483a..fab8e6236 100644
--- a/packages/react-native/package.json
+++ b/packages/react-native/package.json
@@ -19,9 +19,10 @@
"react-native.config.js"
],
"scripts": {
- "build": "tsc -b",
- "build:prod": "tsc -b",
- "clean": "rm -rf lib tsconfig.tsbuildinfo",
+ "prepare:core": "node ../../tools/download-core.js react-native",
+ "build": "pnpm prepare:core && tsc -b",
+ "build:prod": "pnpm prepare:core && tsc -b",
+ "clean": "rm -rf lib tsconfig.tsbuildinfo android/core",
"test": "vitest --config vitest.config.ts",
"watch": "tsc -b -w"
},
diff --git a/tools/download-core.js b/tools/download-core.js
new file mode 100644
index 000000000..a874ff618
--- /dev/null
+++ b/tools/download-core.js
@@ -0,0 +1,181 @@
+// Downloads prebuilt powersync-sqlite-core native libraries from GitHub releases for a given
+// package, verifying each against a pinned hash. Run from the target package's directory:
+//
+// node ../../tools/download-core.js
+//
+// `update_hashes` re-fetches the current hashes for a target's pinned version and prints them,
+// for use after bumping that target's `version` below.
+import { createHash } from 'node:crypto';
+import * as fs from 'node:fs/promises';
+import * as path from 'node:path';
+import { cwd } from 'node:process';
+import { Readable } from 'node:stream';
+import { finished } from 'node:stream/promises';
+
+const version = '0.5.3';
+
+const versionHashes = {
+ 'powersync_x64.dll': 'b3f62293f26d3ee309880d30e1e54819ac21b4d0b1ba9b9177f92e1dcbfdb0c7',
+ 'powersync_x86.dll': 'd2403446c5b2d0550eb99cb195db01798ef78a740ba4f462cd0b887e0ce4bd8a',
+ 'powersync_aarch64.dll': '19afce715bf63b4f590fb2c00d18c3681b818cd6eee5a35284bd3a804f090b20',
+ 'libpowersync_x86.linux.so': 'dc9be35f8a5b5511be8f6b28f8c9d1e00e6ecdb72074ea8e67b826474810276f',
+ 'libpowersync_x64.linux.so': 'a6de0c79151ad6243ca18633f0df10bed32b692095fdf0f01ebdac6eb01d2edf',
+ 'libpowersync_aarch64.linux.so': 'f27b1cadce2210541903ba2f4e9652782c9e14c913cde4f67c463cec00d46d14',
+ 'libpowersync_armv7.linux.so': '3f6ddf0838fbd34e0986e21de0db89b5a0462cdc1eaee95e42c9dcdc1f0c4cc5',
+ 'libpowersync_riscv64gc.linux.so': '5eaa1458153ab562ca0047aa2af4aa3303ee7291eabd92baeb2e9ab2f07935ba',
+ 'libpowersync_x64.macos.dylib': '1209f802bcd886a112bd0de9ca5d7497a62af045fc64b78e703bdd465ee40979',
+ 'libpowersync_aarch64.macos.dylib': '4fa96a98d7edb64a188493beb277bddcb7e85b286e2533d685ecf49ff51c6807',
+
+ 'libpowersync_aarch64.android.so': 'da416aaaaaa6bbc96049a4d28cd36dc5a85b6123bbf8eff18353a491814d43ba',
+ 'libpowersync_armv7.android.so': '4f8ac9fc318f23d931d7ed8d0ba5cb7d4468062ed643c7bc1cb9dafe662d7d10',
+ 'libpowersync_x64.android.so': '901be3196a958b0e5ec4566b7086b1de6f5ec73b78f074bafb51831f9c1a6f45',
+ 'libpowersync_x86.android.so': 'c48de38afbb764971691815b0a20c49eebf2208711da02a08faffd36bee5908e'
+};
+
+const thisScript = import.meta.filename;
+
+// Assets to bundle with @powersync/capacitor and @powersync/react-native as JNI assets.
+const androidAbiAssets = {
+ 'arm64-v8a': 'libpowersync_aarch64.android.so',
+ 'armeabi-v7a': 'libpowersync_armv7.android.so',
+ x86_64: 'libpowersync_x64.android.so',
+ x86: 'libpowersync_x86.android.so'
+};
+
+// All other assets need to be downloaded to node/lib.
+const androidAssets = Object.values(androidAbiAssets);
+const nodeAssets = Object.keys(versionHashes).filter((name) => !androidAssets.includes(name));
+
+const packageDirectory = (...segments) => path.resolve(thisScript, '../../packages', ...segments);
+const hashStream = async (input) => {
+ for await (const chunk of input.pipe(createHash('sha256')).setEncoding('hex')) {
+ return chunk;
+ }
+};
+
+const hashLocal = async (filePath) => {
+ try {
+ const handle = await fs.open(filePath, 'r');
+ const input = handle.createReadStream();
+
+ const result = await hashStream(input);
+ await handle.close();
+ return result;
+ } catch {
+ return null;
+ }
+};
+
+const releaseUrl = (version, asset) =>
+ `https://github.com/powersync-ja/powersync-sqlite-core/releases/download/v${version}/${asset}`;
+
+const relativePath = (absolutePath) => path.normalize(path.relative(cwd(), absolutePath));
+
+const downloadAsset = async (destinationPath, asset) => {
+ const expectedHash = versionHashes[asset];
+
+ await fs.mkdir(path.dirname(destinationPath), { recursive: true });
+
+ const url = releaseUrl(version, asset);
+ console.log(`Downloading ${url}`);
+ const response = await fetch(url);
+ if (response.status != 200) {
+ throw `Could not download ${url}`;
+ }
+
+ const file = await fs.open(destinationPath, 'w');
+ await finished(Readable.fromWeb(response.body).pipe(file.createWriteStream()));
+ await file.close();
+
+ const hashAfterDownloading = await hashLocal(destinationPath);
+ if (hashAfterDownloading != expectedHash) {
+ throw `Unexpected hash after downloading ${asset} (got ${hashAfterDownloading}, expected ${expectedHash})`;
+ }
+
+ console.log(`Successfully downloaded ${asset} to ${relativePath(destinationPath)}`);
+};
+
+const checkAsset = async (destinationPath, asset) => {
+ const expectedHash = versionHashes[asset];
+ const currentHash = await hashLocal(destinationPath);
+
+ return {
+ asset,
+ destinationPath,
+ expectedHash,
+ currentHash,
+ exists: currentHash !== null,
+ isValid: currentHash == expectedHash
+ };
+};
+
+const download = async (files) => {
+ console.log('Checking existing files...');
+ const checks = await Promise.all(files.map(({ path, asset }) => checkAsset(path, asset)));
+
+ const toDownload = checks.filter((check) => !check.isValid);
+ const upToDate = checks.filter((check) => check.isValid);
+
+ if (upToDate.length > 0) {
+ console.log('\nUp-to-date files:');
+ for (const { asset, destinationPath } of upToDate) {
+ console.log(` ✓ ${asset} (in ${relativePath(destinationPath)})`);
+ }
+ }
+
+ if (toDownload.length > 0) {
+ console.log('\nFiles to download:');
+ for (const { asset, exists, destinationPath } of toDownload) {
+ console.log(` • ${asset} (${exists ? 'hash mismatch' : `missing (in ${relativePath(destinationPath)})`})`);
+ }
+
+ console.log('\nStarting downloads...');
+ await Promise.all(toDownload.map(({ asset, destinationPath }) => downloadAsset(destinationPath, asset)));
+
+ console.log('\nAll downloads completed successfully!');
+ } else {
+ console.log('\nAll files are up-to-date, nothing to download.');
+ }
+};
+
+const updateReferenceHashes = async () => {
+ for (const asset of Object.keys(versionHashes)) {
+ const response = await fetch(releaseUrl(target.version, asset));
+ const hash = await hashStream(Readable.fromWeb(response.body));
+
+ console.log(` '${asset}': '${hash}',`);
+ }
+};
+
+const [, , targetName] = process.argv;
+switch (targetName) {
+ case 'update_hashes': {
+ await updateReferenceHashes();
+ break;
+ }
+ case 'node': {
+ await download(
+ nodeAssets.map((asset) => ({
+ asset,
+ path: packageDirectory('node', 'lib', asset)
+ }))
+ );
+ break;
+ }
+ case 'capacitor':
+ case 'react-native': {
+ await download(
+ Object.entries(androidAbiAssets).map(([abi, name]) => {
+ return {
+ asset: name,
+ path: packageDirectory(targetName, 'android', 'core', 'jniLibs', abi, 'libpowersync.so')
+ };
+ })
+ );
+ break;
+ }
+ default: {
+ console.error(`Usage: node download-core.js `);
+ process.exit(1);
+ }
+}