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); + } +}