From d6cb71427938ddf81708480edc4c9bf224c42e03 Mon Sep 17 00:00:00 2001 From: Ngo Quoc Dat Date: Tue, 10 Mar 2026 20:54:57 +0700 Subject: [PATCH 1/2] feat: support per-architecture binaries in plugin registry --- .github/workflows/build-plugin.yml | 29 +++++++++------ .swiftlint.yml | 1 + .../Registry/PluginManager+Registry.swift | 6 ++-- .../Plugins/Registry/RegistryModels.swift | 36 +++++++++++++++++-- docs/development/plugin-registry.mdx | 23 ++++++++---- docs/vi/development/plugin-registry.mdx | 23 ++++++++---- 6 files changed, 89 insertions(+), 29 deletions(-) diff --git a/.github/workflows/build-plugin.yml b/.github/workflows/build-plugin.yml index 56ace63b..6f541aff 100644 --- a/.github/workflows/build-plugin.yml +++ b/.github/workflows/build-plugin.yml @@ -125,9 +125,8 @@ jobs: run: | TAG="${GITHUB_REF#refs/tags/}" - # Registry entry uses arm64 ZIP (Apple Silicon, vast majority of users). - # x86_64 ZIP is available on the release page for manual install. - DOWNLOAD_URL="https://github.com/${{ github.repository }}/releases/download/${TAG}/${{ steps.plugin-info.outputs.bundle_name }}-arm64.zip" + ARM64_URL="https://github.com/${{ github.repository }}/releases/download/${TAG}/${{ steps.plugin-info.outputs.bundle_name }}-arm64.zip" + X86_64_URL="https://github.com/${{ github.repository }}/releases/download/${TAG}/${{ steps.plugin-info.outputs.bundle_name }}-x86_64.zip" # Get current app version as minAppVersion MIN_APP_VERSION=$(sed -n 's/.*MARKETING_VERSION = \(.*\);/\1/p' \ @@ -147,8 +146,10 @@ jobs: "${{ steps.plugin-info.outputs.version }}" \ "${{ steps.plugin-info.outputs.summary }}" \ '${{ steps.plugin-info.outputs.db_type_ids }}' \ - "$DOWNLOAD_URL" \ + "$ARM64_URL" \ "${{ steps.sha256.outputs.arm64 }}" \ + "$X86_64_URL" \ + "${{ steps.sha256.outputs.x86_64 }}" \ "$MIN_APP_VERSION" \ "${{ steps.plugin-info.outputs.icon }}" \ "${{ steps.plugin-info.outputs.homepage }}" \ @@ -160,11 +161,13 @@ jobs: version = sys.argv[3] summary = sys.argv[4] db_type_ids = json.loads(sys.argv[5]) - download_url = sys.argv[6] - sha256 = sys.argv[7] - min_app_version = sys.argv[8] - icon = sys.argv[9] - homepage = sys.argv[10] + arm64_url = sys.argv[6] + arm64_sha = sys.argv[7] + x86_64_url = sys.argv[8] + x86_64_sha = sys.argv[9] + min_app_version = sys.argv[10] + icon = sys.argv[11] + homepage = sys.argv[12] with open("plugins.json", "r") as f: manifest = json.load(f) @@ -178,8 +181,12 @@ jobs: "homepage": homepage, "category": "database-driver", "databaseTypeIds": db_type_ids, - "downloadURL": download_url, - "sha256": sha256, + "downloadURL": arm64_url, + "sha256": arm64_sha, + "binaries": [ + {"architecture": "arm64", "downloadURL": arm64_url, "sha256": arm64_sha}, + {"architecture": "x86_64", "downloadURL": x86_64_url, "sha256": x86_64_sha} + ], "minAppVersion": min_app_version, "minPluginKitVersion": 1, "iconName": icon, diff --git a/.swiftlint.yml b/.swiftlint.yml index e7c12f42..83195ff7 100644 --- a/.swiftlint.yml +++ b/.swiftlint.yml @@ -145,6 +145,7 @@ identifier_name: - protocol_tcp - where_ - TableProTabSmart + - x86_64 nesting: type_level: diff --git a/TablePro/Core/Plugins/Registry/PluginManager+Registry.swift b/TablePro/Core/Plugins/Registry/PluginManager+Registry.swift index 13744f1f..100c4678 100644 --- a/TablePro/Core/Plugins/Registry/PluginManager+Registry.swift +++ b/TablePro/Core/Plugins/Registry/PluginManager+Registry.swift @@ -26,7 +26,9 @@ extension PluginManager { throw PluginError.pluginConflict(existingName: registryPlugin.name) } - guard let downloadURL = URL(string: registryPlugin.downloadURL) else { + let resolved = try registryPlugin.resolvedBinary() + + guard let downloadURL = URL(string: resolved.url) else { throw PluginError.downloadFailed("Invalid download URL") } @@ -57,7 +59,7 @@ extension PluginManager { let digest = SHA256.hash(data: downloadedData) let hexChecksum = digest.map { String(format: "%02x", $0) }.joined() - if hexChecksum != registryPlugin.sha256.lowercased() { + if hexChecksum != resolved.sha256.lowercased() { throw PluginError.checksumMismatch } diff --git a/TablePro/Core/Plugins/Registry/RegistryModels.swift b/TablePro/Core/Plugins/Registry/RegistryModels.swift index 08441379..db67be83 100644 --- a/TablePro/Core/Plugins/Registry/RegistryModels.swift +++ b/TablePro/Core/Plugins/Registry/RegistryModels.swift @@ -5,6 +5,25 @@ import Foundation +enum PluginArchitecture: String, Codable, Sendable { + case arm64 + case x86_64 = "x86_64" + + static var current: PluginArchitecture { + #if arch(arm64) + .arm64 + #else + .x86_64 + #endif + } +} + +struct RegistryBinary: Codable, Sendable { + let architecture: PluginArchitecture + let downloadURL: String + let sha256: String +} + struct RegistryManifest: Codable, Sendable { let schemaVersion: Int let plugins: [RegistryPlugin] @@ -19,14 +38,27 @@ struct RegistryPlugin: Codable, Sendable, Identifiable { let homepage: String? let category: RegistryCategory let databaseTypeIds: [String]? - let downloadURL: String - let sha256: String + let downloadURL: String? + let sha256: String? + let binaries: [RegistryBinary]? let minAppVersion: String? let minPluginKitVersion: Int? let iconName: String? let isVerified: Bool } +extension RegistryPlugin { + func resolvedBinary(for arch: PluginArchitecture = .current) throws -> (url: String, sha256: String) { + if let binaries, let match = binaries.first(where: { $0.architecture == arch }) { + return (match.downloadURL, match.sha256) + } + if let url = downloadURL, let hash = sha256 { + return (url, hash) + } + throw PluginError.noCompatibleBinary + } +} + struct RegistryAuthor: Codable, Sendable { let name: String let url: String? diff --git a/docs/development/plugin-registry.mdx b/docs/development/plugin-registry.mdx index 9b07bfaa..4af26fd6 100644 --- a/docs/development/plugin-registry.mdx +++ b/docs/development/plugin-registry.mdx @@ -32,13 +32,16 @@ Each entry matches the `RegistryPlugin` model: | `homepage` | `string` | No | Project URL | | `category` | `string` | Yes | One of: `database-driver`, `export-format`, `import-format`, `theme`, `other` | | `databaseTypeIds` | `[string]` | No | Maps to `DatabaseType.pluginTypeId` values. Used for auto-install prompts. | -| `downloadURL` | `string` | Yes | Direct URL to the `.zip` file | -| `sha256` | `string` | Yes | SHA-256 hex digest of the ZIP file | +| `downloadURL` | `string` | No* | Direct URL to the `.zip` file | +| `sha256` | `string` | No* | SHA-256 hex digest of the ZIP file | +| `binaries` | `[object]` | No | Per-architecture binaries: `[{ "architecture": "arm64"\|"x86_64", "downloadURL": "...", "sha256": "..." }]` | | `minAppVersion` | `string` | No | Minimum TablePro version (e.g., `"0.17.0"`) | | `minPluginKitVersion` | `int` | No | Minimum PluginKit version (currently `1`) | | `iconName` | `string` | No | SF Symbol name for display | | `isVerified` | `bool` | Yes | Whether the plugin is verified by the TablePro team | +\* Either `downloadURL`/`sha256` (flat fields) or `binaries` array is required. If `binaries` is present, the app picks the matching architecture. Flat fields serve as fallback for older app versions. + ## Oracle and ClickHouse Entries These two database drivers ship as downloadable plugins instead of being bundled with the app. Their registry entries: @@ -58,6 +61,10 @@ These two database drivers ship as downloadable plugins instead of being bundled "databaseTypeIds": ["Oracle"], "downloadURL": "https://github.com/datlechin/TablePro/releases/download/plugin-oracle-v1.0.0/OracleDriver-arm64.zip", "sha256": "", + "binaries": [ + { "architecture": "arm64", "downloadURL": "https://github.com/datlechin/TablePro/releases/download/plugin-oracle-v1.0.0/OracleDriver-arm64.zip", "sha256": "" }, + { "architecture": "x86_64", "downloadURL": "https://github.com/datlechin/TablePro/releases/download/plugin-oracle-v1.0.0/OracleDriver-x86_64.zip", "sha256": "" } + ], "minAppVersion": "0.17.0", "minPluginKitVersion": 1, "iconName": "server.rack", @@ -80,6 +87,10 @@ These two database drivers ship as downloadable plugins instead of being bundled "databaseTypeIds": ["ClickHouse"], "downloadURL": "https://github.com/datlechin/TablePro/releases/download/plugin-clickhouse-v1.0.0/ClickHouseDriver-arm64.zip", "sha256": "", + "binaries": [ + { "architecture": "arm64", "downloadURL": "https://github.com/datlechin/TablePro/releases/download/plugin-clickhouse-v1.0.0/ClickHouseDriver-arm64.zip", "sha256": "" }, + { "architecture": "x86_64", "downloadURL": "https://github.com/datlechin/TablePro/releases/download/plugin-clickhouse-v1.0.0/ClickHouseDriver-x86_64.zip", "sha256": "" } + ], "minAppVersion": "0.17.0", "minPluginKitVersion": 1, "iconName": "chart.bar.xaxis", @@ -87,7 +98,7 @@ These two database drivers ship as downloadable plugins instead of being bundled } ``` -Replace `` with the actual SHA-256 output from `build-plugin.sh`. You need one entry per architecture, or a universal ZIP. +Replace `` with the actual SHA-256 output from `build-plugin.sh`. The `binaries` array provides per-architecture downloads; `downloadURL`/`sha256` flat fields point to arm64 as fallback for older app versions. ## databaseTypeIds Mapping @@ -107,10 +118,8 @@ The `databaseTypeIds` field maps registry entries to `DatabaseType.pluginTypeId` ## Publishing a Plugin Release 1. Tag the commit: `git tag plugin-oracle-v1.0.0 && git push --tags` -2. The `build-plugin.yml` CI workflow builds, signs, and creates a GitHub release with ZIP artifacts -3. Copy the SHA-256 from the build output -4. Add or update the entry in `plugins.json` on the [TableProApp/plugins](https://github.com/TableProApp/plugins) repo -5. The app fetches the updated manifest on next **Browse** visit (with ETag caching) +2. The `build-plugin.yml` CI workflow builds both architectures (arm64 + x86_64), signs, creates a GitHub release with ZIP artifacts, and automatically updates the plugin registry with a `binaries` array +3. The app fetches the updated manifest on next **Browse** visit (with ETag caching) ## Auto-Install Flow diff --git a/docs/vi/development/plugin-registry.mdx b/docs/vi/development/plugin-registry.mdx index 2dd9a037..03e73b86 100644 --- a/docs/vi/development/plugin-registry.mdx +++ b/docs/vi/development/plugin-registry.mdx @@ -32,13 +32,16 @@ Mỗi mục tương ứng với model `RegistryPlugin`: | `homepage` | `string` | Không | URL dự án | | `category` | `string` | Có | Một trong: `database-driver`, `export-format`, `import-format`, `theme`, `other` | | `databaseTypeIds` | `[string]` | Không | Ánh xạ đến giá trị `DatabaseType.pluginTypeId`. Dùng cho tự động cài đặt. | -| `downloadURL` | `string` | Có | URL trực tiếp đến file `.zip` | -| `sha256` | `string` | Có | SHA-256 hex digest của file ZIP | +| `downloadURL` | `string` | Không* | URL trực tiếp đến file `.zip` | +| `sha256` | `string` | Không* | SHA-256 hex digest của file ZIP | +| `binaries` | `[object]` | Không | Binary theo kiến trúc: `[{ "architecture": "arm64"\|"x86_64", "downloadURL": "...", "sha256": "..." }]` | | `minAppVersion` | `string` | Không | Phiên bản TablePro tối thiểu (vd: `"0.17.0"`) | | `minPluginKitVersion` | `int` | Không | Phiên bản PluginKit tối thiểu (hiện tại là `1`) | | `iconName` | `string` | Không | Tên SF Symbol để hiển thị | | `isVerified` | `bool` | Có | Plugin đã được đội ngũ TablePro xác minh hay chưa | +\* Cần có `downloadURL`/`sha256` (trường phẳng) hoặc mảng `binaries`. Nếu có `binaries`, ứng dụng chọn kiến trúc phù hợp. Trường phẳng dùng làm fallback cho phiên bản ứng dụng cũ. + ## Mục Oracle và ClickHouse Hai database driver này được phân phối dưới dạng plugin tải về thay vì đóng gói trong ứng dụng: @@ -58,6 +61,10 @@ Hai database driver này được phân phối dưới dạng plugin tải về "databaseTypeIds": ["Oracle"], "downloadURL": "https://github.com/datlechin/TablePro/releases/download/plugin-oracle-v1.0.0/OracleDriver-arm64.zip", "sha256": "", + "binaries": [ + { "architecture": "arm64", "downloadURL": "https://github.com/datlechin/TablePro/releases/download/plugin-oracle-v1.0.0/OracleDriver-arm64.zip", "sha256": "" }, + { "architecture": "x86_64", "downloadURL": "https://github.com/datlechin/TablePro/releases/download/plugin-oracle-v1.0.0/OracleDriver-x86_64.zip", "sha256": "" } + ], "minAppVersion": "0.17.0", "minPluginKitVersion": 1, "iconName": "server.rack", @@ -80,6 +87,10 @@ Hai database driver này được phân phối dưới dạng plugin tải về "databaseTypeIds": ["ClickHouse"], "downloadURL": "https://github.com/datlechin/TablePro/releases/download/plugin-clickhouse-v1.0.0/ClickHouseDriver-arm64.zip", "sha256": "", + "binaries": [ + { "architecture": "arm64", "downloadURL": "https://github.com/datlechin/TablePro/releases/download/plugin-clickhouse-v1.0.0/ClickHouseDriver-arm64.zip", "sha256": "" }, + { "architecture": "x86_64", "downloadURL": "https://github.com/datlechin/TablePro/releases/download/plugin-clickhouse-v1.0.0/ClickHouseDriver-x86_64.zip", "sha256": "" } + ], "minAppVersion": "0.17.0", "minPluginKitVersion": 1, "iconName": "chart.bar.xaxis", @@ -87,7 +98,7 @@ Hai database driver này được phân phối dưới dạng plugin tải về } ``` -Thay `` bằng SHA-256 thực tế từ output của `build-plugin.sh`. +Thay `` bằng SHA-256 thực tế từ output của `build-plugin.sh`. Mảng `binaries` cung cấp tải về theo kiến trúc; trường phẳng `downloadURL`/`sha256` trỏ đến arm64 làm fallback cho phiên bản ứng dụng cũ. ## Ánh xạ databaseTypeIds @@ -107,10 +118,8 @@ Trường `databaseTypeIds` ánh xạ mục registry đến giá trị `Database ## Xuất bản Plugin 1. Tạo tag: `git tag plugin-oracle-v1.0.0 && git push --tags` -2. CI workflow `build-plugin.yml` sẽ build, ký và tạo GitHub release với file ZIP -3. Sao chép SHA-256 từ output build -4. Thêm hoặc cập nhật mục trong `plugins.json` trên repo [TableProApp/plugins](https://github.com/TableProApp/plugins) -5. Ứng dụng sẽ tải manifest mới khi mở **Duyệt** (có cache ETag) +2. CI workflow `build-plugin.yml` sẽ build cả hai kiến trúc (arm64 + x86_64), ký, tạo GitHub release với file ZIP, và tự động cập nhật registry plugin với mảng `binaries` +3. Ứng dụng sẽ tải manifest mới khi mở **Duyệt** (có cache ETag) ## Luồng tự động cài đặt From 907bbe8b40df6d5de4b77fe0dee5c9b88a4e6cfe Mon Sep 17 00:00:00 2001 From: Ngo Quoc Dat Date: Tue, 10 Mar 2026 21:01:06 +0700 Subject: [PATCH 2/2] fix: address review feedback on multi-arch plugin registry --- .github/workflows/build-plugin.yml | 2 ++ TablePro/Core/Plugins/Registry/RegistryModels.swift | 2 +- docs/vi/development/plugin-registry.mdx | 8 ++++---- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/.github/workflows/build-plugin.yml b/.github/workflows/build-plugin.yml index 6f541aff..a7bd7773 100644 --- a/.github/workflows/build-plugin.yml +++ b/.github/workflows/build-plugin.yml @@ -81,6 +81,8 @@ jobs: run: ./scripts/build-plugin.sh "${{ steps.plugin-info.outputs.target }}" x86_64 - name: Capture SHA-256 hashes + # Hashes are captured before notarize intentionally: notarytool does not + # modify the ZIP on disk, and stapling applies to app bundles, not ZIPs. id: sha256 run: | BUNDLE_NAME="${{ steps.plugin-info.outputs.bundle_name }}" diff --git a/TablePro/Core/Plugins/Registry/RegistryModels.swift b/TablePro/Core/Plugins/Registry/RegistryModels.swift index db67be83..a50cd745 100644 --- a/TablePro/Core/Plugins/Registry/RegistryModels.swift +++ b/TablePro/Core/Plugins/Registry/RegistryModels.swift @@ -7,7 +7,7 @@ import Foundation enum PluginArchitecture: String, Codable, Sendable { case arm64 - case x86_64 = "x86_64" + case x86_64 static var current: PluginArchitecture { #if arch(arm64) diff --git a/docs/vi/development/plugin-registry.mdx b/docs/vi/development/plugin-registry.mdx index 03e73b86..63bf43c5 100644 --- a/docs/vi/development/plugin-registry.mdx +++ b/docs/vi/development/plugin-registry.mdx @@ -62,8 +62,8 @@ Hai database driver này được phân phối dưới dạng plugin tải về "downloadURL": "https://github.com/datlechin/TablePro/releases/download/plugin-oracle-v1.0.0/OracleDriver-arm64.zip", "sha256": "", "binaries": [ - { "architecture": "arm64", "downloadURL": "https://github.com/datlechin/TablePro/releases/download/plugin-oracle-v1.0.0/OracleDriver-arm64.zip", "sha256": "" }, - { "architecture": "x86_64", "downloadURL": "https://github.com/datlechin/TablePro/releases/download/plugin-oracle-v1.0.0/OracleDriver-x86_64.zip", "sha256": "" } + { "architecture": "arm64", "downloadURL": "https://github.com/datlechin/TablePro/releases/download/plugin-oracle-v1.0.0/OracleDriver-arm64.zip", "sha256": "" }, + { "architecture": "x86_64", "downloadURL": "https://github.com/datlechin/TablePro/releases/download/plugin-oracle-v1.0.0/OracleDriver-x86_64.zip", "sha256": "" } ], "minAppVersion": "0.17.0", "minPluginKitVersion": 1, @@ -88,8 +88,8 @@ Hai database driver này được phân phối dưới dạng plugin tải về "downloadURL": "https://github.com/datlechin/TablePro/releases/download/plugin-clickhouse-v1.0.0/ClickHouseDriver-arm64.zip", "sha256": "", "binaries": [ - { "architecture": "arm64", "downloadURL": "https://github.com/datlechin/TablePro/releases/download/plugin-clickhouse-v1.0.0/ClickHouseDriver-arm64.zip", "sha256": "" }, - { "architecture": "x86_64", "downloadURL": "https://github.com/datlechin/TablePro/releases/download/plugin-clickhouse-v1.0.0/ClickHouseDriver-x86_64.zip", "sha256": "" } + { "architecture": "arm64", "downloadURL": "https://github.com/datlechin/TablePro/releases/download/plugin-clickhouse-v1.0.0/ClickHouseDriver-arm64.zip", "sha256": "" }, + { "architecture": "x86_64", "downloadURL": "https://github.com/datlechin/TablePro/releases/download/plugin-clickhouse-v1.0.0/ClickHouseDriver-x86_64.zip", "sha256": "" } ], "minAppVersion": "0.17.0", "minPluginKitVersion": 1,