diff --git a/.github/workflows/dev-packages.yml b/.github/workflows/dev-packages.yml index e63e2fe6..dedef039 100644 --- a/.github/workflows/dev-packages.yml +++ b/.github/workflows/dev-packages.yml @@ -24,7 +24,7 @@ jobs: with: dotnet-version: "10.0.x" - - name: Install MAUI Workloads + - name: Install native workloads run: dotnet workload restore - name: Download PowerSync extension @@ -33,9 +33,6 @@ jobs: - name: Restore dependencies run: dotnet restore - - name: Install Android SDK for net8.0-android - run: dotnet build PowerSync/PowerSync.Common -t:InstallAndroidDependencies -f net8.0-android -p:AndroidSdkDirectory="$ANDROID_HOME" -p:AcceptAndroidSdkLicenses=True - - name: Extract Common Package Version from CHANGELOG.md shell: bash run: | @@ -50,20 +47,6 @@ jobs: - name: Run Pack For Common run: dotnet pack PowerSync/PowerSync.Common -c Release -o ${{ github.workspace }}/output - - name: Extract MAUI Package Version from CHANGELOG.md - shell: bash - run: | - MAUI_VERSION=$(awk '/^## [0-9]+\.[0-9]+\.[0-9]+-dev(\.[0-9]+)?$/ {print $2; exit}' PowerSync/PowerSync.Maui/CHANGELOG.md) - if [[ -z "$MAUI_VERSION" ]]; then - echo "Error: Invalid dev version found in PowerSync.Maui/CHANGELOG.md. Expected format: x.x.x-dev.x" - exit 1 - fi - echo "Detected Version: $MAUI_VERSION" - echo "VERSION=$MAUI_VERSION" >> $GITHUB_ENV - - - name: Run Pack For MAUI - run: dotnet pack PowerSync/PowerSync.Maui -c Release -o ${{ github.workspace }}/output - - name: Upload Build Artifacts uses: actions/upload-artifact@v4 with: @@ -90,7 +73,7 @@ jobs: - name: Setup .NET uses: actions/setup-dotnet@v4 with: - dotnet-version: "8.0.x" + dotnet-version: "10.0.x" - name: Install Sign CLI tool run: dotnet tool install --tool-path . --prerelease sign @@ -142,7 +125,7 @@ jobs: - name: Setup .NET uses: actions/setup-dotnet@v4 with: - dotnet-version: "8.0.x" + dotnet-version: "10.0.x" - name: Validate artifact signatures run: dotnet nuget verify ${{ github.workspace }}/SignedArtifacts/**/*.nupkg @@ -158,9 +141,3 @@ jobs: dotnet nuget push "${{ github.workspace }}\SignedArtifacts\PowerSync.Common*.nupkg" --api-key "${{ steps.login.outputs.NUGET_API_KEY }}" --source https://api.nuget.org/v3/index.json - - - name: Run Push For Maui - run: > - dotnet nuget push "${{ github.workspace }}\SignedArtifacts\PowerSync.Maui*.nupkg" - --api-key "${{ steps.login.outputs.NUGET_API_KEY }}" - --source https://api.nuget.org/v3/index.json diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 71c0874a..dfd311e4 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -15,7 +15,6 @@ jobs: outputs: common_version: ${{ steps.extract_version.outputs.common_version }} - maui_version: ${{ steps.extract_maui_version.outputs.maui_version }} steps: - name: Checkout Repository @@ -28,7 +27,7 @@ jobs: with: dotnet-version: "10.0.x" - - name: Install MAUI Workloads + - name: Install native workloads run: dotnet workload restore - name: Download PowerSync extension @@ -37,9 +36,6 @@ jobs: - name: Restore dependencies run: dotnet restore - - name: Install Android SDK for net8.0-android - run: dotnet build PowerSync/PowerSync.Common -t:InstallAndroidDependencies -f net8.0-android -p:AndroidSdkDirectory="$ANDROID_HOME" -p:AcceptAndroidSdkLicenses=True - - name: Extract Common Package Version from CHANGELOG.md id: extract_version shell: bash @@ -56,22 +52,6 @@ jobs: - name: Run Pack For Common run: dotnet pack PowerSync/PowerSync.Common -c Release -o ${{ github.workspace }}/output - - name: Extract MAUI Package Version from CHANGELOG.md - id: extract_maui_version - shell: bash - run: | - MAUI_VERSION=$(awk '/^## [0-9]+\.[0-9]+\.[0-9]+/ {print $2; exit}' PowerSync/PowerSync.Maui/CHANGELOG.md) - if [[ -z "$MAUI_VERSION" ]]; then - echo "Error: Invalid version found in PowerSync.Maui/CHANGELOG.md. Expected format: '## x.x.x'" - exit 1 - fi - echo "Detected Version: $MAUI_VERSION" - echo "VERSION=$MAUI_VERSION" >> $GITHUB_ENV - echo "maui_version=$MAUI_VERSION" >> $GITHUB_OUTPUT - - - name: Run Pack For MAUI - run: dotnet pack PowerSync/PowerSync.Maui -c Release -o ${{ github.workspace }}/output - - name: Upload Build Artifacts uses: actions/upload-artifact@v4 with: @@ -98,7 +78,7 @@ jobs: - name: Setup .NET uses: actions/setup-dotnet@v4 with: - dotnet-version: "8.0.x" + dotnet-version: "10.0.x" - name: Install Sign CLI tool run: dotnet tool install --tool-path . --prerelease sign @@ -150,7 +130,7 @@ jobs: - name: Setup .NET uses: actions/setup-dotnet@v4 with: - dotnet-version: "8.0.x" + dotnet-version: "10.0.x" - name: Validate artifact signatures run: dotnet nuget verify ${{ github.workspace }}/SignedArtifacts/**/*.nupkg @@ -181,24 +161,3 @@ jobs: --target main \ --generate-notes \ SignedArtifacts/PowerSync.Common*.nupkg - - - name: Run Push For Maui - run: > - dotnet nuget push "${{ github.workspace }}\SignedArtifacts\PowerSync.Maui*.nupkg" - --api-key "${{ steps.login.outputs.NUGET_API_KEY }}" - --source https://api.nuget.org/v3/index.json - - - name: Create GitHub Release For Maui - continue-on-error: true - shell: bash - env: - GH_TOKEN: ${{ github.token }} - GH_REPO: ${{ github.repository }} - MAUI_VERSION: ${{ needs.build.outputs.maui_version }} - run: | - gh release create "PowerSync.Maui@${MAUI_VERSION}" \ - --draft \ - --title "PowerSync.Maui@${MAUI_VERSION}" \ - --target main \ - --generate-notes \ - SignedArtifacts/PowerSync.Maui*.nupkg diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 110b65f1..a2920b91 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -6,7 +6,7 @@ on: jobs: build: strategy: - matrix: + matrix: runner: [windows-latest, windows-11-arm] name: Test Packages ${{ matrix.runner }} runs-on: ${{ matrix.runner }} @@ -18,16 +18,16 @@ jobs: - name: Setup .NET SDK uses: actions/setup-dotnet@v5 with: - dotnet-version: '10.0' + dotnet-version: "10.0" - - name: Install MAUI Workloads + - name: Install native workloads run: dotnet workload restore - + - name: Download PowerSync extension - run: dotnet run --project Tools/Setup + run: dotnet run --project Tools/Setup - name: Restore dependencies run: dotnet restore - + - name: Run tests run: dotnet test -v n --framework net10.0 diff --git a/.gitignore b/.gitignore index 688d8e68..48437c78 100644 --- a/.gitignore +++ b/.gitignore @@ -58,13 +58,8 @@ TestResults/ *.db-shm *.db-wal -#Core binaries - add rules after ps extension downloading is supported -*.dylib -*.dll -*.so -*.xcframework -.env -*NativeLibs +# PowerSync runtimes installed by Tools/Setup +**/runtimes/ # Ignore user id file user_id.txt diff --git a/PowerSync/PowerSync.Common/MDSQLite/MDSQLiteAdapter.cs b/PowerSync/PowerSync.Common/MDSQLite/MDSQLiteAdapter.cs index cdb4ec9e..7fa4d16e 100644 --- a/PowerSync/PowerSync.Common/MDSQLite/MDSQLiteAdapter.cs +++ b/PowerSync/PowerSync.Common/MDSQLite/MDSQLiteAdapter.cs @@ -12,6 +12,10 @@ namespace PowerSync.Common.MDSQLite; using PowerSync.Common.DB; using PowerSync.Common.Utils; +#if IOS || MACCATALYST +using Foundation; +#endif + public class MDSQLiteAdapterOptions() { public string Name { get; set; } = null!; @@ -154,13 +158,25 @@ protected virtual void LoadExtensions(SqliteConnection db) /// /// Loads the bundled PowerSync core SQLite extension. Override on - /// platform-specific adapters (e.g. MAUI iOS/Android) where the native library - /// lives outside the desktop runtime path. + /// platform-specific adapters where the native library lives + /// outside the standard runtimes path. /// protected virtual void LoadDefaultPowerSyncExtension(SqliteConnection db) { - var path = PowerSyncPathResolver.GetNativeLibraryPath(AppContext.BaseDirectory); - db.LoadExtension(path, "sqlite3_powersync_init"); + string extensionPath; + +#if IOS || MACCATALYST + var bundlePath = + NSBundle.FromIdentifier("co.powersync.sqlitecore")?.BundlePath + ?? throw new Exception("Could not find PowerSync SQLite extension bundle path"); + extensionPath = Path.Combine(bundlePath, "powersync-sqlite-core"); +#elif ANDROID + extensionPath = "libpowersync"; +#else + extensionPath = PowerSyncDesktopPathResolver.GetNativeLibraryPath(AppContext.BaseDirectory); +#endif + + db.LoadExtension(extensionPath, "sqlite3_powersync_init"); } public async Task Close() diff --git a/PowerSync/PowerSync.Common/PowerSync.Common.csproj b/PowerSync/PowerSync.Common/PowerSync.Common.csproj index 7310aa85..68a2b46f 100644 --- a/PowerSync/PowerSync.Common/PowerSync.Common.csproj +++ b/PowerSync/PowerSync.Common/PowerSync.Common.csproj @@ -38,26 +38,40 @@ + - - + - - - - + + + + PreserveNewest - - - - + + + + + PreserveNewest + + + + + PreserveNewest + + + + + + + + - + diff --git a/PowerSync/PowerSync.Common/PowerSync.Common.targets b/PowerSync/PowerSync.Common/PowerSync.Common.targets deleted file mode 100644 index 0c5bcba2..00000000 --- a/PowerSync/PowerSync.Common/PowerSync.Common.targets +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/PowerSync/PowerSync.Common/Utils/PowerSyncPathResolver.cs b/PowerSync/PowerSync.Common/Utils/PowerSyncDesktopPathResolver.cs similarity index 97% rename from PowerSync/PowerSync.Common/Utils/PowerSyncPathResolver.cs rename to PowerSync/PowerSync.Common/Utils/PowerSyncDesktopPathResolver.cs index 8aa7c3a4..f6c07f77 100644 --- a/PowerSync/PowerSync.Common/Utils/PowerSyncPathResolver.cs +++ b/PowerSync/PowerSync.Common/Utils/PowerSyncDesktopPathResolver.cs @@ -2,7 +2,7 @@ namespace PowerSync.Common.Utils; using System.Runtime.InteropServices; -public static class PowerSyncPathResolver +public static class PowerSyncDesktopPathResolver { public static string GetNativeLibraryPath(string packagePath) { diff --git a/PowerSync/PowerSync.Maui/CHANGELOG.md b/PowerSync/PowerSync.Maui/CHANGELOG.md index 09ce912c..39bffd2f 100644 --- a/PowerSync/PowerSync.Maui/CHANGELOG.md +++ b/PowerSync/PowerSync.Maui/CHANGELOG.md @@ -2,8 +2,7 @@ ## 1.0.1 -- Full release. -- Upstream PowerSync.Common version bump (See Powersync.Common changelog 1.0.1 for more information) +- ⚠️ Deprecate `PowerSync.Maui`. Use `PowerSync.Common` and `MDSQLiteAdapter` / `MDSQLiteDBOpenFactory` instead. ## 1.0.0 (unlisted) diff --git a/PowerSync/PowerSync.Maui/PowerSync.Maui.csproj b/PowerSync/PowerSync.Maui/PowerSync.Maui.csproj index 751532b2..a8a4703e 100644 --- a/PowerSync/PowerSync.Maui/PowerSync.Maui.csproj +++ b/PowerSync/PowerSync.Maui/PowerSync.Maui.csproj @@ -1,5 +1,4 @@  - net9.0;net10.0;net9.0-android;net10.0-android $(TargetFrameworks);net9.0-ios;net10.0-ios;net9.0-maccatalyst;net10.0-maccatalyst @@ -19,51 +18,15 @@ https://github.com/powersync-ja/powersync-dotnet/PowerSync/PowerSync.Maui/CHANGELOG.md powersync local-first local-storage state-management offline sql db persistence sqlite sync icon.png - NU5100;1591 README.md - true - true - + - - Designer - - - - - - - - - Framework - False - - - - - - - Framework - False - - - - - - - - - - diff --git a/PowerSync/PowerSync.Maui/README.md b/PowerSync/PowerSync.Maui/README.md index bc75882c..af6137a4 100644 --- a/PowerSync/PowerSync.Maui/README.md +++ b/PowerSync/PowerSync.Maui/README.md @@ -1,45 +1,3 @@ # PowerSync SDK .NET MAUI -This package provides .NET Multi-platform App UI (MAUI) integration for PowerSync, designed to work with the PowerSync.Common package for cross-platform mobile and desktop applications. - -## ⚠️ Project Status & Release Note - -This package is in beta and is considered ready for production use for tested use cases. See our feature status definitions [here](https://docs.powersync.com/resources/feature-status). - -## Installation - -This package is published on [NuGet](https://www.nuget.org/packages/PowerSync.Maui) and requires PowerSync.Common to also be installed. - -```bash -dotnet add package PowerSync.Maui -dotnet add package PowerSync.Common -``` - -## Usage - -Initialization differs slightly from our Common SDK when using MAUI. - -```csharp - -private record ListResult(string id, string name, string owner_id, string created_at); - -static async Task Main() { - - // Ensures the DB file is stored in a platform appropriate location - var dbPath = Path.Combine(FileSystem.AppDataDirectory, "maui-example.db"); - var factory = new MAUISQLiteDBOpenFactory(new MDSQLiteOpenFactoryOptions() - { - DbFilename = dbPath - }); - - var Db = new PowerSyncDatabase(new PowerSyncDatabaseOptions() - { - Database = factory, // Supply a factory - Schema = AppSchema.PowerSyncSchema, - }); - - await db.Init(); - - var lists = await db.GetAll("select * from lists"); -} -``` +- ⚠️ This package is deprecated. Use `PowerSync.Common` and `MDSQLiteAdapter` / `MDSQLiteDBOpenFactory` instead. diff --git a/PowerSync/PowerSync.Maui/SQLite/MAUISQLiteAdapter.cs b/PowerSync/PowerSync.Maui/SQLite/MAUISQLiteAdapter.cs deleted file mode 100644 index 0d6e3993..00000000 --- a/PowerSync/PowerSync.Maui/SQLite/MAUISQLiteAdapter.cs +++ /dev/null @@ -1,55 +0,0 @@ -namespace PowerSync.Maui.SQLite; - -using Microsoft.Data.Sqlite; - -using PowerSync.Common.MDSQLite; - -// iOS/MacCatalyst specific imports -#if IOS || MACCATALYST -using Foundation; -#endif - -public class MAUISQLiteAdapter : MDSQLiteAdapter -{ - public MAUISQLiteAdapter(MDSQLiteAdapterOptions options) : base(options) - { - } - - // The bundled PowerSync extension lives in a platform-specific location on - // iOS/MacCatalyst/Android — the desktop runtime path used by the base class - // does not resolve to it. Override only the PowerSync-extension load hook; - // user-supplied custom extensions still flow through MDSQLiteAdapter.LoadExtensions - // unchanged, so consumers can freely combine the bundled extension (via the - // LoadPowerSyncExtension flag) with their own. - protected override void LoadDefaultPowerSyncExtension(SqliteConnection db) - { -#if IOS || MACCATALYST - LoadExtensionApple(db); -#elif ANDROID - db.LoadExtension("libpowersync"); -#else - base.LoadDefaultPowerSyncExtension(db); -#endif - } - - private static void LoadExtensionApple(SqliteConnection db) - { -#if IOS || MACCATALYST - var bundlePath = Foundation.NSBundle.FromIdentifier("co.powersync.sqlitecore")?.BundlePath; - if (bundlePath == null) - { - throw new Exception("Could not find PowerSync SQLite extension bundle path"); - } - - var filePath = - Path.Combine(bundlePath, "powersync-sqlite-core"); - - using var loadExtension = db.CreateCommand(); - loadExtension.CommandText = "SELECT load_extension(@path, @entryPoint)"; - loadExtension.Parameters.AddWithValue("@path", filePath); - loadExtension.Parameters.AddWithValue("@entryPoint", "sqlite3_powersync_init"); - loadExtension.ExecuteNonQuery(); -#endif - } -} - diff --git a/PowerSync/PowerSync.Maui/SQLite/MAUISQLiteDBOpenFactory.cs b/PowerSync/PowerSync.Maui/SQLite/MAUISQLiteDBOpenFactory.cs deleted file mode 100644 index c8d35088..00000000 --- a/PowerSync/PowerSync.Maui/SQLite/MAUISQLiteDBOpenFactory.cs +++ /dev/null @@ -1,26 +0,0 @@ -using PowerSync.Common.DB; - -namespace PowerSync.Maui.SQLite; - -using PowerSync.Common.Client; -using PowerSync.Common.MDSQLite; - - -public class MAUISQLiteDBOpenFactory : ISQLOpenFactory -{ - private readonly MDSQLiteOpenFactoryOptions options; - - public MAUISQLiteDBOpenFactory(MDSQLiteOpenFactoryOptions options) - { - this.options = options; - } - - public IDBAdapter OpenDatabase() - { - return new MAUISQLiteAdapter(new MDSQLiteAdapterOptions - { - Name = options.DbFilename, - SqliteOptions = options.SqliteOptions - }); - } -} diff --git a/PowerSync/PowerSync.Maui/build/ApiDefinition.cs b/PowerSync/PowerSync.Maui/build/ApiDefinition.cs deleted file mode 100644 index 5fe8ef43..00000000 --- a/PowerSync/PowerSync.Maui/build/ApiDefinition.cs +++ /dev/null @@ -1,4 +0,0 @@ -namespace PowerSync.Maui.build -{ - // Empty API definition - allows xcframework to be included without managed bindings -} diff --git a/Tests/PowerSync/PowerSync.Common.Tests/MDSQLite/MDSQLiteAdapterTests.cs b/Tests/PowerSync/PowerSync.Common.Tests/MDSQLite/MDSQLiteAdapterTests.cs index 41843828..507a173c 100644 --- a/Tests/PowerSync/PowerSync.Common.Tests/MDSQLite/MDSQLiteAdapterTests.cs +++ b/Tests/PowerSync/PowerSync.Common.Tests/MDSQLite/MDSQLiteAdapterTests.cs @@ -53,7 +53,7 @@ public async Task DisablingCoreExtensionPreventsPowerSyncFromLoading() public async Task LoadsCustomPowerSyncExtensionFromOverriddenPath() { var dbName = $"MDSQLiteAdapter-{Guid.NewGuid():N}.db"; - var sourcePath = PowerSyncPathResolver.GetNativeLibraryPath(AppContext.BaseDirectory); + var sourcePath = PowerSyncDesktopPathResolver.GetNativeLibraryPath(AppContext.BaseDirectory); var customPath = Path.Combine( Path.GetTempPath(), $"powersync-ext-copy-{Guid.NewGuid():N}{Path.GetExtension(sourcePath)}" diff --git a/Tools/Setup/Setup.cs b/Tools/Setup/Setup.cs index c91d2ecd..ddca0e46 100644 --- a/Tools/Setup/Setup.cs +++ b/Tools/Setup/Setup.cs @@ -1,245 +1,260 @@ -using System; -using System.IO; -using System.Net.Http; -using System.Threading.Tasks; -using System.Collections.Generic; -using System.IO.Compression; - -/// -/// Execute with `dotnet run --project Tools/Setup` -/// -public class PowerSyncSetup -{ - private const string VERSION = "0.5.2"; - - private const string GITHUB_BASE_URL = $"https://github.com/powersync-ja/powersync-sqlite-core/releases/download/v{VERSION}"; - - private readonly HttpClient _httpClient; - private readonly string _basePath; - - public PowerSyncSetup() - { - _httpClient = new HttpClient(); - _basePath = Path.Combine(AppContext.BaseDirectory, "../../../../..", "PowerSync"); - } - - public async Task RunSetup() - { - try - { - await SetupDesktop(); - await SetupMauiIos(); - await SetupMauiAndroid(); - await SetupMauiMacCatalyst(); - } - finally - { - _httpClient?.Dispose(); - } - } - - public async Task SetupDesktop() - { - Console.WriteLine("Setting up Desktop libraries..."); - - var runtimeConfigs = GetDesktopRuntimeConfigs(); - var commonPath = Path.Combine(_basePath, "PowerSync.Common"); - - foreach (var config in runtimeConfigs) - { - await ProcessDesktopRuntime(commonPath, config); - } - } - - private static Dictionary GetDesktopRuntimeConfigs() - { - return new Dictionary - { - { "osx-x64", new RuntimeConfig("libpowersync_x64.macos.dylib", "libpowersync.dylib") }, - { "osx-arm64", new RuntimeConfig("libpowersync_aarch64.macos.dylib", "libpowersync.dylib") }, - { "linux-x64", new RuntimeConfig("libpowersync_x64.linux.so", "libpowersync.so") }, - { "linux-arm64", new RuntimeConfig("libpowersync_aarch64.linux.so", "libpowersync.so") }, - { "win-x64", new RuntimeConfig("powersync_x64.dll", "powersync.dll") }, - { "win-arm64", new RuntimeConfig("powersync_aarch64.dll", "powersync.dll") } - }; - } - - private async Task ProcessDesktopRuntime(string basePath, KeyValuePair runtimeConfig) - { - var (rid, config) = runtimeConfig; - var nativeDir = Path.Combine(basePath, "runtimes", rid, "native"); - - try - { - Directory.CreateDirectory(nativeDir); - - var downloadPath = Path.Combine(nativeDir, config.OriginalFileName); - var finalPath = Path.Combine(nativeDir, config.FinalFileName); - - var downloadUrl = $"{GITHUB_BASE_URL}/{config.OriginalFileName}"; - - await DownloadFile(downloadUrl, downloadPath); - File.Move(downloadPath, finalPath, overwrite: true); - - Console.WriteLine($"✓ {rid}: {config.OriginalFileName} → {config.FinalFileName}"); - } - catch (Exception ex) - { - Console.Error.WriteLine($"✗ Failed to process {rid}: {ex.Message}"); - } - } - - public async Task SetupMauiIos() - { - Console.WriteLine("Setting up MAUI iOS libraries..."); - - var nativeDir = Path.Combine(_basePath, "PowerSync.Maui", "Platforms", "iOS", "NativeLibs"); - var config = new ArchiveConfig( - "powersync-sqlite-core.xcframework.zip", - "powersync-sqlite-core.xcframework" - ); - - await ProcessArchiveDownload(nativeDir, config, GITHUB_BASE_URL); - } - - public async Task SetupMauiAndroid() - { - Console.WriteLine("Setting up MAUI Android libraries..."); - - var nativeDir = Path.Combine(_basePath, "PowerSync.Maui", "Platforms", "Android", "jniLibs"); - - try - { - Directory.CreateDirectory(nativeDir); - - await Task.WhenAll( - DownloadAndroidLibrary("libpowersync_aarch64.android.so ", nativeDir,"arm64-v8a"), - DownloadAndroidLibrary("libpowersync_armv7.android.so ", nativeDir, "armeabi-v7a"), - DownloadAndroidLibrary("libpowersync_x86.android.so ", nativeDir, "x86"), - DownloadAndroidLibrary("libpowersync_x64.android.so ", nativeDir, "x86_64") - ); - - Console.WriteLine($"✓ Android: Downloaded native libraries"); - } - catch (Exception ex) - { - Console.Error.WriteLine($"✗ Failed to setup Android: {ex.Message}"); - } - } - - private async Task DownloadAndroidLibrary(string filename, string jniLibsDir, string arch) - { - var targetDir = Path.Combine(jniLibsDir, arch); - Directory.CreateDirectory(targetDir); - var targetFile = Path.Combine(targetDir, "libpowersync.so"); - await DownloadFile($"{GITHUB_BASE_URL}/{filename}", targetFile); - } - - public async Task SetupMauiMacCatalyst() - { - Console.WriteLine("Setting up MAUI MacCatalyst libraries..."); - - var nativeDir = Path.Combine(_basePath, "PowerSync.Maui", "Platforms", "MacCatalyst", "NativeLibs"); - var config = new ArchiveConfig( - "powersync-sqlite-core.xcframework.zip", - "powersync-sqlite-core.xcframework" - ); - - await ProcessArchiveDownload(nativeDir, config, GITHUB_BASE_URL); - } - - private async Task ProcessArchiveDownload(string nativeDir, ArchiveConfig config, string baseUrl) - { - try - { - Directory.CreateDirectory(nativeDir); - - var downloadPath = Path.Combine(nativeDir, config.ArchiveFileName); - var extractedPath = Path.Combine(nativeDir, config.ExtractedName); - var downloadUrl = $"{baseUrl}/{config.ArchiveFileName}"; - - await DownloadFile(downloadUrl, downloadPath); - - // Clean up existing extraction - if (Directory.Exists(extractedPath)) - Directory.Delete(extractedPath, recursive: true); - - ExtractZipPreservingSymlinks(downloadPath, nativeDir); - File.Delete(downloadPath); - - Console.WriteLine($"✓ Extracted {config.ArchiveFileName} → {config.ExtractedName}"); - } - catch (Exception ex) - { - Console.Error.WriteLine($"✗ Failed to process archive: {ex.Message}"); - } - } - - private static void ExtractZipPreservingSymlinks(string zipPath, string destDir) - { - // ZipFile.ExtractToDirectory does not preserve symlinks, which breaks - // macOS/Catalyst .xcframework bundles. Use `unzip` on Unix instead. - if (!OperatingSystem.IsWindows()) - { - var proc = System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo - { - FileName = "unzip", - ArgumentList = { "-o", zipPath, "-d", destDir }, - RedirectStandardOutput = true, - RedirectStandardError = true, - })!; - proc.WaitForExit(); - if (proc.ExitCode != 0) - throw new Exception($"unzip exited with code {proc.ExitCode}: {proc.StandardError.ReadToEnd()}"); - } - else - { - ZipFile.ExtractToDirectory(zipPath, destDir); - } - } - - private async Task DownloadFile(string url, string outputPath) - { - Console.WriteLine($"📥 Downloading: {Path.GetFileName(outputPath)}"); - - using var response = await _httpClient.GetAsync(url); - - if (!response.IsSuccessStatusCode) - { - throw new HttpRequestException($"Download failed: {response.StatusCode} {response.ReasonPhrase}"); - } - - await using var fileStream = new FileStream(outputPath, FileMode.Create, FileAccess.Write, FileShare.None); - await response.Content.CopyToAsync(fileStream); - } - - private static void CleanupPaths(params string[] paths) - { - foreach (var path in paths) - { - try - { - if (File.Exists(path)) - File.Delete(path); - else if (Directory.Exists(path)) - Directory.Delete(path, recursive: true); - } - catch (Exception ex) - { - Console.WriteLine($"⚠️ Cleanup warning for {path}: {ex.Message}"); - } - } - } - - private record RuntimeConfig(string OriginalFileName, string FinalFileName); - private record ArchiveConfig(string ArchiveFileName, string ExtractedName); -} - -public class Program -{ - static async Task Main(string[] args) - { - var setup = new PowerSyncSetup(); - await setup.RunSetup(); - } -} +using System; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Threading.Tasks; +using System.Collections.Generic; +using System.IO.Compression; +using System.Xml; +using System.Xml.Linq; + +/// +/// Downloads the powersync-sqlite-core native libraries into PowerSync.Common/runtimes, +/// laid out using NuGet's RID conventions so a single package can serve every target: +/// +/// runtimes/{rid}/native/... desktop and Android, resolved by RID +/// runtimes/ios/native/*.xcframework.zip +/// runtimes/maccatalyst/native/*.xcframework.zip +/// +/// The Apple slices stay zipped on purpose. NuGet pack/extract and ordinary file copies +/// drop symlinks, which corrupts the versioned MacCatalyst framework bundle and makes +/// codesign fail with "bundle format is ambiguous". The Apple SDK unzips these itself +/// and embeds the framework into App.app/Frameworks. +/// +/// Execute with `dotnet run --project Tools/Setup` +/// +public class PowerSyncSetup +{ + private const string VERSION = "0.5.2"; + + private const string GITHUB_BASE_URL = $"https://github.com/powersync-ja/powersync-sqlite-core/releases/download/v{VERSION}"; + + private const string XCFRAMEWORK_NAME = "powersync-sqlite-core.xcframework"; + + private readonly HttpClient _httpClient; + private readonly string _commonPath; + + public PowerSyncSetup() + { + _httpClient = new HttpClient(); + _commonPath = Path.Combine(AppContext.BaseDirectory, "../../../../..", "PowerSync", "PowerSync.Common"); + } + + public async Task RunSetup() + { + try + { + await SetupRuntimes(); + await SetupApple(); + } + finally + { + _httpClient?.Dispose(); + } + } + + /// + /// Every target whose native library is a single downloadable file: one entry per RID. + /// + public async Task SetupRuntimes() + { + Console.WriteLine("Setting up native libraries..."); + + foreach (var config in GetRuntimeConfigs()) + { + await ProcessRuntime(config); + } + } + + private static Dictionary GetRuntimeConfigs() + { + return new Dictionary + { + // Desktop + { "osx-x64", new RuntimeConfig("libpowersync_x64.macos.dylib", "libpowersync.dylib") }, + { "osx-arm64", new RuntimeConfig("libpowersync_aarch64.macos.dylib", "libpowersync.dylib") }, + { "linux-x64", new RuntimeConfig("libpowersync_x64.linux.so", "libpowersync.so") }, + { "linux-arm64", new RuntimeConfig("libpowersync_aarch64.linux.so", "libpowersync.so") }, + { "win-x64", new RuntimeConfig("powersync_x64.dll", "powersync.dll") }, + { "win-arm64", new RuntimeConfig("powersync_aarch64.dll", "powersync.dll") }, + + // Android. These reach the APK's ABI directories via AndroidNativeLibrary items + // in PowerSync.Common.csproj (ProjectReference) and NuGet RID resolution (PackageReference). + { "android-arm64", new RuntimeConfig("libpowersync_aarch64.android.so", "libpowersync.so") }, + { "android-x64", new RuntimeConfig("libpowersync_x64.android.so", "libpowersync.so") }, + { "android-arm", new RuntimeConfig("libpowersync_armv7.android.so", "libpowersync.so") }, + { "android-x86", new RuntimeConfig("libpowersync_x86.android.so", "libpowersync.so") }, + }; + } + + private async Task ProcessRuntime(KeyValuePair runtimeConfig) + { + var (rid, config) = runtimeConfig; + var nativeDir = Path.Combine(_commonPath, "runtimes", rid, "native"); + + try + { + Directory.CreateDirectory(nativeDir); + + var downloadPath = Path.Combine(nativeDir, config.OriginalFileName); + var finalPath = Path.Combine(nativeDir, config.FinalFileName); + + var downloadUrl = $"{GITHUB_BASE_URL}/{config.OriginalFileName}"; + + await DownloadFile(downloadUrl, downloadPath); + File.Move(downloadPath, finalPath, overwrite: true); + + Console.WriteLine($"✓ {rid}: {config.OriginalFileName} → {config.FinalFileName}"); + } + catch (Exception ex) + { + Console.Error.WriteLine($"✗ Failed to process {rid}: {ex.Message}"); + } + } + + /// + /// Apple ships one xcframework covering every Apple platform, so it needs its own path: + /// download once, then write a per-platform copy carrying only the slices that platform + /// needs. The published archive is ~135MB — it includes tvOS, watchOS and macOS slices we + /// do not target, plus a dSYM per slice — which trims down to a few MB. + /// + public async Task SetupApple() + { + Console.WriteLine("Setting up Apple libraries..."); + + var tempZip = Path.Combine(Path.GetTempPath(), $"{XCFRAMEWORK_NAME}.zip"); + + try + { + await DownloadFile($"{GITHUB_BASE_URL}/{XCFRAMEWORK_NAME}.zip", tempZip); + + WriteTrimmedXcframework(tempZip, "ios", ["ios-arm64", "ios-arm64_x86_64-simulator"]); + WriteTrimmedXcframework(tempZip, "maccatalyst", ["ios-arm64_x86_64-maccatalyst"]); + } + catch (Exception ex) + { + Console.Error.WriteLine($"✗ Failed to process Apple libraries: {ex.Message}"); + } + finally + { + if (File.Exists(tempZip)) File.Delete(tempZip); + } + } + + private void WriteTrimmedXcframework(string sourceZip, string rid, string[] keepSlices) + { + var nativeDir = Path.Combine(_commonPath, "runtimes", rid, "native"); + Directory.CreateDirectory(nativeDir); + + var targetZip = Path.Combine(nativeDir, $"{XCFRAMEWORK_NAME}.zip"); + if (File.Exists(targetZip)) File.Delete(targetZip); + + using var source = ZipFile.OpenRead(sourceZip); + using var targetStream = new FileStream(targetZip, FileMode.Create, FileAccess.Write, FileShare.None); + using var target = new ZipArchive(targetStream, ZipArchiveMode.Create); + + var infoPlistPath = $"{XCFRAMEWORK_NAME}/Info.plist"; + + foreach (var entry in source.Entries) + { + // Debug symbols are the bulk of the download and are not needed to build against. + if (entry.FullName.Contains("/dSYMs/")) continue; + + var slice = GetSliceName(entry.FullName); + if (slice != null && !keepSlices.Contains(slice)) continue; + + // Copying the raw bytes plus ExternalAttributes is what preserves symlinks: they + // are stored as entries whose content is the link target and whose unix mode bits + // live in ExternalAttributes. Extracting and re-zipping would lose them. + var copy = target.CreateEntry(entry.FullName, CompressionLevel.Optimal); + copy.ExternalAttributes = entry.ExternalAttributes; + copy.LastWriteTime = entry.LastWriteTime; + + using var output = copy.Open(); + + if (entry.FullName == infoPlistPath) + { + using var plistStream = entry.Open(); + using var trimmed = TrimInfoPlist(plistStream, keepSlices); + trimmed.CopyTo(output); + } + else + { + using var input = entry.Open(); + input.CopyTo(output); + } + } + + Console.WriteLine($"✓ {rid}: {XCFRAMEWORK_NAME}.zip ({string.Join(", ", keepSlices)})"); + } + + /// + /// Returns the xcframework slice a zip entry belongs to, or null for entries that sit + /// outside a slice (the root Info.plist, LICENSE, README). + /// + private static string? GetSliceName(string entryPath) + { + var parts = entryPath.Split('/'); + if (parts.Length < 3 || parts[0] != XCFRAMEWORK_NAME) return null; + return parts[1]; + } + + /// + /// Drops the removed slices from the xcframework's AvailableLibraries list, so the Apple + /// SDK does not go looking for slices that are no longer present in the archive. + /// + private static Stream TrimInfoPlist(Stream plistStream, string[] keepSlices) + { + // XmlResolver is null so the Apple DTD named by the DOCTYPE is never fetched, while + // DtdProcessing.Parse keeps the DOCTYPE itself in the output. + var settings = new XmlReaderSettings { DtdProcessing = DtdProcessing.Parse, XmlResolver = null }; + using var reader = XmlReader.Create(plistStream, settings); + var doc = XDocument.Load(reader); + + var librariesArray = doc.Root?.Element("dict") + ?.Elements("key") + .FirstOrDefault(k => k.Value == "AvailableLibraries") + ?.NextNode as XElement; + + librariesArray?.Elements("dict") + .Where(d => !keepSlices.Contains(PlistString(d, "LibraryIdentifier"))) + .Remove(); + + var output = new MemoryStream(); + doc.Save(output); + output.Position = 0; + return output; + } + + private static string? PlistString(XElement dict, string key) + { + var keyElement = dict.Elements("key").FirstOrDefault(k => k.Value == key); + return (keyElement?.NextNode as XElement)?.Value; + } + + private async Task DownloadFile(string url, string outputPath) + { + Console.WriteLine($"📥 Downloading: {Path.GetFileName(outputPath)}"); + + using var response = await _httpClient.GetAsync(url); + + if (!response.IsSuccessStatusCode) + { + throw new HttpRequestException($"Download failed: {response.StatusCode} {response.ReasonPhrase}"); + } + + await using var fileStream = new FileStream(outputPath, FileMode.Create, FileAccess.Write, FileShare.None); + await response.Content.CopyToAsync(fileStream); + } + + private record RuntimeConfig(string OriginalFileName, string FinalFileName); +} + +public class Program +{ + static async Task Main(string[] args) + { + var setup = new PowerSyncSetup(); + await setup.RunSetup(); + } +} diff --git a/demos/MAUITodo/Data/PowerSyncData.cs b/demos/MAUITodo/Data/PowerSyncData.cs index 676781f8..818e20c6 100644 --- a/demos/MAUITodo/Data/PowerSyncData.cs +++ b/demos/MAUITodo/Data/PowerSyncData.cs @@ -8,7 +8,6 @@ using PowerSync.Common.Attachments; using PowerSync.Common.Client; using PowerSync.Common.MDSQLite; -using PowerSync.Maui.SQLite; namespace MAUITodo.Data; @@ -29,7 +28,7 @@ public PowerSyncData() var logger = loggerFactory.CreateLogger("PowerSyncLogger"); var dbPath = Path.Combine(FileSystem.AppDataDirectory, "example.db"); - var factory = new MAUISQLiteDBOpenFactory(new MDSQLiteOpenFactoryOptions() + var factory = new MDSQLiteDBOpenFactory(new MDSQLiteOpenFactoryOptions() { DbFilename = dbPath }); diff --git a/demos/MAUITodo/MAUITodo.csproj b/demos/MAUITodo/MAUITodo.csproj index 9f38150b..ee955424 100644 --- a/demos/MAUITodo/MAUITodo.csproj +++ b/demos/MAUITodo/MAUITodo.csproj @@ -1,5 +1,4 @@  - com.companyname.todo @@ -64,6 +63,5 @@ -