diff --git a/.claude/rules/plugin-system.md b/.claude/rules/plugin-system.md index de573eb205..0d52c96e8a 100644 --- a/.claude/rules/plugin-system.md +++ b/.claude/rules/plugin-system.md @@ -24,9 +24,10 @@ TableProPluginKit is built with Library Evolution, so an already-built plugin ke - Add a requirement only with a default implementation, then bridge it in `PluginDriverAdapter`. - Additive: a new requirement with a default, a new field on a non-`@frozen` struct, a new case on a non-`@frozen` enum, a reordering. - Breaking: a changed, removed or renamed requirement or signature, a requirement without a default, a new case on a `@frozen` enum, a change to a frozen type's layout. -- Every ABI change bumps `currentPluginKitVersion` (in `PluginManager.swift`) and `TableProPluginKitVersion` in every plugin `Info.plist`, at most once per release cycle: later changes in the cycle reuse the pending number. `scripts/ci/check-pluginkit-bump-cadence.py` checks it in Repo Hygiene. +- Every ABI change bumps `TABLEPRO_PLUGINKIT_VERSION` in `Configs/PluginKit.xcconfig`, at most once per release cycle: later changes in the cycle reuse the pending number. `scripts/ci/check-pluginkit-bump-cadence.py` checks it in Repo Hygiene. +- The build stamps that value into `TableProPluginKitVersion` of the app and of every plugin. A plugin's `Info.plist` holds `TABLEPRO_PLUGINKIT_VERSION` and is preprocessed, because every shipped app reads the key with `as? Int`; the source file therefore fails `plutil -lint`. - An additive change re-publishes nothing. Built plugins keep loading, and the bump makes an older app refuse a plugin rebuilt against the new API instead of failing its load. -- A breaking change also raises `minimumCompatiblePluginKitVersion`, and every registry plugin is re-released with `scripts/release-all-plugins.sh ` before the app tag, because the release's `Registry Readiness` job fails while a registry driver has no binary in the app's kit range (`RELEASING.md`, The release pipeline). +- A breaking change also raises `TABLEPRO_PLUGINKIT_MINIMUM_VERSION`, and every registry plugin is re-released with `scripts/release-all-plugins.sh ` before the app tag, because the release's `Registry Readiness` job fails while a registry driver has no binary in the app's kit range (`RELEASING.md`, The release pipeline). - Check with `verify.sh abi ` (`scripts/check-pluginkit-abi.sh`). It builds PluginKit at HEAD and at the base with the same toolchain and diffs the public `.swiftinterface`. A base with no interface passes; a diff prints the additive and breaking rules and exits 1. - It refuses uncommitted changes, so run it after the commit and before the push. No workflow runs it. - The diff is textual, so a `-` line can be a declaration that moved. Only a removed line with no matching added line, or a requirement added without a default, is breaking. This lists the removed lines that do not come back: diff --git a/.github/workflows/build-plugin.yml b/.github/workflows/build-plugin.yml index 6dfcfff2ea..e36897cfaa 100644 --- a/.github/workflows/build-plugin.yml +++ b/.github/workflows/build-plugin.yml @@ -8,7 +8,7 @@ on: tags: description: > Comma-separated plugin tag:pluginKitVersion pairs. - pluginKitVersion defaults to currentPluginKitVersion from PluginManager.swift. + pluginKitVersion defaults to TABLEPRO_PLUGINKIT_VERSION from Configs/PluginKit.xcconfig. Examples: plugin-mongodb-v1.0.25, plugin-mongodb-v1.0.25:13 required: true type: string @@ -21,7 +21,7 @@ on: required: false type: string -# Declared per job rather than here. resolve-tags reads two integers out of a Swift file and needs +# Declared per job rather than here. resolve-tags reads an integer out of an xcconfig and needs # nothing, and a workflow-level grant hands it a write token it has no use for. permissions: contents: read @@ -66,18 +66,17 @@ jobs: - name: Check the plugin manifest against the plugin classes run: python3 scripts/ci/check-plugin-manifest.py - - name: Read currentPluginKitVersion + - name: Read the PluginKit version id: pkv run: | - VERSION=$(grep -E 'static let currentPluginKitVersion\s*=\s*[0-9]+' \ - TablePro/Core/Plugins/PluginManager.swift \ - | grep -oE '[0-9]+$' | head -1) + VERSION=$(sed -nE 's/^TABLEPRO_PLUGINKIT_VERSION[[:space:]]*=[[:space:]]*([0-9]+)[[:space:]]*$/\1/p' \ + Configs/PluginKit.xcconfig) if [ -z "$VERSION" ]; then - echo "::error::Could not parse currentPluginKitVersion from PluginManager.swift" + echo "::error::Could not parse TABLEPRO_PLUGINKIT_VERSION from Configs/PluginKit.xcconfig" exit 1 fi echo "version=$VERSION" >> "$GITHUB_OUTPUT" - echo "currentPluginKitVersion=$VERSION" + echo "TABLEPRO_PLUGINKIT_VERSION=$VERSION" - name: Build matrix id: tags diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f77343fafc..14e249bfa2 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -185,7 +185,7 @@ jobs: build/Release/TablePro-*.dmg build/Release/TablePro-*.tar.xz - # Reads two integers out of a Swift file and fetches a JSON manifest, so it wants nothing + # Reads two integers out of an xcconfig and fetches a JSON manifest, so it wants nothing # from macOS and does not need to spend a macOS runner minute (billed at ten times Linux). registry-readiness: name: Registry Readiness @@ -199,9 +199,9 @@ jobs: - name: Verify registry has compatible plugin binaries run: | - MANAGER="TablePro/Core/Plugins/PluginManager.swift" - CURRENT=$(grep -E 'static let currentPluginKitVersion = ' "$MANAGER" | grep -oE '[0-9]+' | head -1) - FLOOR=$(grep -E 'static let minimumCompatiblePluginKitVersion = ' "$MANAGER" | grep -oE '[0-9]+' | head -1) + CONFIG="Configs/PluginKit.xcconfig" + CURRENT=$(sed -nE 's/^TABLEPRO_PLUGINKIT_VERSION[[:space:]]*=[[:space:]]*([0-9]+)[[:space:]]*$/\1/p' "$CONFIG") + FLOOR=$(sed -nE 's/^TABLEPRO_PLUGINKIT_MINIMUM_VERSION[[:space:]]*=[[:space:]]*([0-9]+)[[:space:]]*$/\1/p' "$CONFIG") echo "PluginKit floor=$FLOOR current=$CURRENT" python3 scripts/check-registry-readiness.py --floor "$FLOOR" --current "$CURRENT" diff --git a/Configs/Base.xcconfig b/Configs/Base.xcconfig index 3f39b27db6..323f91bffe 100644 --- a/Configs/Base.xcconfig +++ b/Configs/Base.xcconfig @@ -2,6 +2,8 @@ // Structure rule: project-wide and per-configuration settings live in Configs/*.xcconfig. // Per-target settings live in project.yml, where the target templates are. +#include "PluginKit.xcconfig" + ALWAYS_SEARCH_USER_PATHS = NO ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES CLANG_ANALYZER_NONNULL = YES @@ -60,10 +62,9 @@ STRINGS_FILE_OUTPUT_ENCODING = binary SWIFT_VERSION = 6.0 CODE_SIGN_STYLE = Automatic -// Signing knobs. Only the two app targets read them, so plugin bundles and the helper -// tool sign ad-hoc and a contributor without the official team can still build everything. -// A contributor overrides these in Configs/Secrets.xcconfig without touching a tracked file, -// and the release scripts pass their own values on the xcodebuild command line. +// Signing knobs. The app targets and the plugin bundles sign with this team; the helper tool +// signs ad hoc. A contributor overrides these in Configs/Secrets.xcconfig without touching a +// tracked file, and the release scripts pass their own values on the xcodebuild command line. TABLEPRO_DEVELOPMENT_TEAM = D7HJ5TFYCU TABLEPRO_APP_BUNDLE_IDENTIFIER = com.TablePro diff --git a/Configs/PluginKit.xcconfig b/Configs/PluginKit.xcconfig new file mode 100644 index 0000000000..9f5ac3b3d0 --- /dev/null +++ b/Configs/PluginKit.xcconfig @@ -0,0 +1,3 @@ +// When to raise each, and how the build stamps them: .claude/rules/plugin-system.md, ABI. +TABLEPRO_PLUGINKIT_VERSION = 39 +TABLEPRO_PLUGINKIT_MINIMUM_VERSION = 19 diff --git a/Plugins/BeancountDriverPlugin/Info.plist b/Plugins/BeancountDriverPlugin/Info.plist index 5eb0eeab09..8bb97777cc 100644 --- a/Plugins/BeancountDriverPlugin/Info.plist +++ b/Plugins/BeancountDriverPlugin/Info.plist @@ -3,7 +3,7 @@ TableProPluginKitVersion - 39 + TABLEPRO_PLUGINKIT_VERSION TableProProvidesDatabaseTypeIds Beancount diff --git a/Plugins/BigQueryDriverPlugin/Info.plist b/Plugins/BigQueryDriverPlugin/Info.plist index 88fdb3660c..ad014bef07 100644 --- a/Plugins/BigQueryDriverPlugin/Info.plist +++ b/Plugins/BigQueryDriverPlugin/Info.plist @@ -3,6 +3,6 @@ TableProPluginKitVersion - 39 + TABLEPRO_PLUGINKIT_VERSION diff --git a/Plugins/BigtableDriverPlugin/Info.plist b/Plugins/BigtableDriverPlugin/Info.plist index b1ef933cfe..6953716526 100644 --- a/Plugins/BigtableDriverPlugin/Info.plist +++ b/Plugins/BigtableDriverPlugin/Info.plist @@ -3,7 +3,7 @@ TableProPluginKitVersion - 39 + TABLEPRO_PLUGINKIT_VERSION TableProProvidesDatabaseTypeIds Bigtable diff --git a/Plugins/CSVExportPlugin/Info.plist b/Plugins/CSVExportPlugin/Info.plist index bc844bd4ea..81a04239c9 100644 --- a/Plugins/CSVExportPlugin/Info.plist +++ b/Plugins/CSVExportPlugin/Info.plist @@ -3,7 +3,7 @@ TableProPluginKitVersion - 39 + TABLEPRO_PLUGINKIT_VERSION TableProProvidesExportFormatIds csv diff --git a/Plugins/CSVImportPlugin/Info.plist b/Plugins/CSVImportPlugin/Info.plist index c92042694f..a09c94dbca 100644 --- a/Plugins/CSVImportPlugin/Info.plist +++ b/Plugins/CSVImportPlugin/Info.plist @@ -3,7 +3,7 @@ TableProPluginKitVersion - 39 + TABLEPRO_PLUGINKIT_VERSION TableProProvidesImportFormatIds csv diff --git a/Plugins/CassandraDriverPlugin/Info.plist b/Plugins/CassandraDriverPlugin/Info.plist index 88fdb3660c..ad014bef07 100644 --- a/Plugins/CassandraDriverPlugin/Info.plist +++ b/Plugins/CassandraDriverPlugin/Info.plist @@ -3,6 +3,6 @@ TableProPluginKitVersion - 39 + TABLEPRO_PLUGINKIT_VERSION diff --git a/Plugins/ClickHouseDriverPlugin/Info.plist b/Plugins/ClickHouseDriverPlugin/Info.plist index 5189083c91..10fa22e650 100644 --- a/Plugins/ClickHouseDriverPlugin/Info.plist +++ b/Plugins/ClickHouseDriverPlugin/Info.plist @@ -3,7 +3,7 @@ TableProPluginKitVersion - 39 + TABLEPRO_PLUGINKIT_VERSION TableProProvidesDatabaseTypeIds ClickHouse diff --git a/Plugins/CloudflareD1DriverPlugin/Info.plist b/Plugins/CloudflareD1DriverPlugin/Info.plist index 88fdb3660c..ad014bef07 100644 --- a/Plugins/CloudflareD1DriverPlugin/Info.plist +++ b/Plugins/CloudflareD1DriverPlugin/Info.plist @@ -3,6 +3,6 @@ TableProPluginKitVersion - 39 + TABLEPRO_PLUGINKIT_VERSION diff --git a/Plugins/CloudflareR2SQLDriverPlugin/Info.plist b/Plugins/CloudflareR2SQLDriverPlugin/Info.plist index edd28fb2d7..9625b89836 100644 --- a/Plugins/CloudflareR2SQLDriverPlugin/Info.plist +++ b/Plugins/CloudflareR2SQLDriverPlugin/Info.plist @@ -3,7 +3,7 @@ TableProPluginKitVersion - 39 + TABLEPRO_PLUGINKIT_VERSION TableProProvidesDatabaseTypeIds Cloudflare R2 SQL diff --git a/Plugins/DamengDriverPlugin/Info.plist b/Plugins/DamengDriverPlugin/Info.plist index 767f6b3e09..024e929450 100644 --- a/Plugins/DamengDriverPlugin/Info.plist +++ b/Plugins/DamengDriverPlugin/Info.plist @@ -3,7 +3,7 @@ TableProPluginKitVersion - 39 + TABLEPRO_PLUGINKIT_VERSION TableProProvidesDatabaseTypeIds Dameng diff --git a/Plugins/DuckDBDriverPlugin/Info.plist b/Plugins/DuckDBDriverPlugin/Info.plist index 88fdb3660c..ad014bef07 100644 --- a/Plugins/DuckDBDriverPlugin/Info.plist +++ b/Plugins/DuckDBDriverPlugin/Info.plist @@ -3,6 +3,6 @@ TableProPluginKitVersion - 39 + TABLEPRO_PLUGINKIT_VERSION diff --git a/Plugins/DynamoDBDriverPlugin/Info.plist b/Plugins/DynamoDBDriverPlugin/Info.plist index 88fdb3660c..ad014bef07 100644 --- a/Plugins/DynamoDBDriverPlugin/Info.plist +++ b/Plugins/DynamoDBDriverPlugin/Info.plist @@ -3,6 +3,6 @@ TableProPluginKitVersion - 39 + TABLEPRO_PLUGINKIT_VERSION diff --git a/Plugins/ElasticsearchDriverPlugin/Info.plist b/Plugins/ElasticsearchDriverPlugin/Info.plist index 88fdb3660c..ad014bef07 100644 --- a/Plugins/ElasticsearchDriverPlugin/Info.plist +++ b/Plugins/ElasticsearchDriverPlugin/Info.plist @@ -3,6 +3,6 @@ TableProPluginKitVersion - 39 + TABLEPRO_PLUGINKIT_VERSION diff --git a/Plugins/EtcdDriverPlugin/Info.plist b/Plugins/EtcdDriverPlugin/Info.plist index 88fdb3660c..ad014bef07 100644 --- a/Plugins/EtcdDriverPlugin/Info.plist +++ b/Plugins/EtcdDriverPlugin/Info.plist @@ -3,6 +3,6 @@ TableProPluginKitVersion - 39 + TABLEPRO_PLUGINKIT_VERSION diff --git a/Plugins/FirebaseDriverPlugin/Info.plist b/Plugins/FirebaseDriverPlugin/Info.plist index 8edfb96f65..0a260443b2 100644 --- a/Plugins/FirebaseDriverPlugin/Info.plist +++ b/Plugins/FirebaseDriverPlugin/Info.plist @@ -3,7 +3,7 @@ TableProPluginKitVersion - 39 + TABLEPRO_PLUGINKIT_VERSION TableProProvidesDatabaseTypeIds Firestore diff --git a/Plugins/HanaDriverPlugin/Info.plist b/Plugins/HanaDriverPlugin/Info.plist index 2494774fd1..37b21f2e8d 100644 --- a/Plugins/HanaDriverPlugin/Info.plist +++ b/Plugins/HanaDriverPlugin/Info.plist @@ -3,7 +3,7 @@ TableProPluginKitVersion - 39 + TABLEPRO_PLUGINKIT_VERSION TableProProvidesDatabaseTypeIds SAP HANA diff --git a/Plugins/JSONExportPlugin/Info.plist b/Plugins/JSONExportPlugin/Info.plist index ceea251c9b..4077dfde3a 100644 --- a/Plugins/JSONExportPlugin/Info.plist +++ b/Plugins/JSONExportPlugin/Info.plist @@ -3,7 +3,7 @@ TableProPluginKitVersion - 39 + TABLEPRO_PLUGINKIT_VERSION TableProProvidesExportFormatIds json diff --git a/Plugins/JSONImportPlugin/Info.plist b/Plugins/JSONImportPlugin/Info.plist index f8fb2ead03..d0beb83840 100644 --- a/Plugins/JSONImportPlugin/Info.plist +++ b/Plugins/JSONImportPlugin/Info.plist @@ -3,7 +3,7 @@ TableProPluginKitVersion - 39 + TABLEPRO_PLUGINKIT_VERSION TableProProvidesImportFormatIds json diff --git a/Plugins/KafkaDriverPlugin/Info.plist b/Plugins/KafkaDriverPlugin/Info.plist index 06a6c6dba0..2f105003d2 100644 --- a/Plugins/KafkaDriverPlugin/Info.plist +++ b/Plugins/KafkaDriverPlugin/Info.plist @@ -3,7 +3,7 @@ TableProPluginKitVersion - 39 + TABLEPRO_PLUGINKIT_VERSION TableProProvidesDatabaseTypeIds Kafka diff --git a/Plugins/LibSQLDriverPlugin/Info.plist b/Plugins/LibSQLDriverPlugin/Info.plist index 88fdb3660c..ad014bef07 100644 --- a/Plugins/LibSQLDriverPlugin/Info.plist +++ b/Plugins/LibSQLDriverPlugin/Info.plist @@ -3,6 +3,6 @@ TableProPluginKitVersion - 39 + TABLEPRO_PLUGINKIT_VERSION diff --git a/Plugins/MQLExportPlugin/Info.plist b/Plugins/MQLExportPlugin/Info.plist index 29ea7006fe..7cc82fecc2 100644 --- a/Plugins/MQLExportPlugin/Info.plist +++ b/Plugins/MQLExportPlugin/Info.plist @@ -3,7 +3,7 @@ TableProPluginKitVersion - 39 + TABLEPRO_PLUGINKIT_VERSION TableProProvidesExportFormatIds mql diff --git a/Plugins/MSSQLDriverPlugin/Info.plist b/Plugins/MSSQLDriverPlugin/Info.plist index 88fdb3660c..ad014bef07 100644 --- a/Plugins/MSSQLDriverPlugin/Info.plist +++ b/Plugins/MSSQLDriverPlugin/Info.plist @@ -3,6 +3,6 @@ TableProPluginKitVersion - 39 + TABLEPRO_PLUGINKIT_VERSION diff --git a/Plugins/MarkdownExportPlugin/Info.plist b/Plugins/MarkdownExportPlugin/Info.plist index cb1c7d34de..9d9b33d127 100644 --- a/Plugins/MarkdownExportPlugin/Info.plist +++ b/Plugins/MarkdownExportPlugin/Info.plist @@ -3,7 +3,7 @@ TableProPluginKitVersion - 39 + TABLEPRO_PLUGINKIT_VERSION TableProProvidesExportFormatIds md diff --git a/Plugins/MongoDBDriverPlugin/Info.plist b/Plugins/MongoDBDriverPlugin/Info.plist index 88fdb3660c..ad014bef07 100644 --- a/Plugins/MongoDBDriverPlugin/Info.plist +++ b/Plugins/MongoDBDriverPlugin/Info.plist @@ -3,6 +3,6 @@ TableProPluginKitVersion - 39 + TABLEPRO_PLUGINKIT_VERSION diff --git a/Plugins/MySQLDriverPlugin/Info.plist b/Plugins/MySQLDriverPlugin/Info.plist index 2cca4834c1..e736a2c92e 100644 --- a/Plugins/MySQLDriverPlugin/Info.plist +++ b/Plugins/MySQLDriverPlugin/Info.plist @@ -3,7 +3,7 @@ TableProPluginKitVersion - 39 + TABLEPRO_PLUGINKIT_VERSION TableProProvidesDatabaseTypeIds MySQL diff --git a/Plugins/OracleDriverPlugin/Info.plist b/Plugins/OracleDriverPlugin/Info.plist index 88fdb3660c..ad014bef07 100644 --- a/Plugins/OracleDriverPlugin/Info.plist +++ b/Plugins/OracleDriverPlugin/Info.plist @@ -3,6 +3,6 @@ TableProPluginKitVersion - 39 + TABLEPRO_PLUGINKIT_VERSION diff --git a/Plugins/ParquetExportPlugin/Info.plist b/Plugins/ParquetExportPlugin/Info.plist index 0d5a128b54..758650d5c3 100644 --- a/Plugins/ParquetExportPlugin/Info.plist +++ b/Plugins/ParquetExportPlugin/Info.plist @@ -3,7 +3,7 @@ TableProPluginKitVersion - 39 + TABLEPRO_PLUGINKIT_VERSION TableProProvidesExportFormatIds parquet diff --git a/Plugins/PostgreSQLDriverPlugin/Info.plist b/Plugins/PostgreSQLDriverPlugin/Info.plist index b118efa2ba..27c87e63de 100644 --- a/Plugins/PostgreSQLDriverPlugin/Info.plist +++ b/Plugins/PostgreSQLDriverPlugin/Info.plist @@ -3,7 +3,7 @@ TableProPluginKitVersion - 39 + TABLEPRO_PLUGINKIT_VERSION TableProProvidesDatabaseTypeIds PostgreSQL diff --git a/Plugins/RedisDriverPlugin/Info.plist b/Plugins/RedisDriverPlugin/Info.plist index e2085f42ec..3bb3e5f4b7 100644 --- a/Plugins/RedisDriverPlugin/Info.plist +++ b/Plugins/RedisDriverPlugin/Info.plist @@ -3,7 +3,7 @@ TableProPluginKitVersion - 39 + TABLEPRO_PLUGINKIT_VERSION TableProProvidesDatabaseTypeIds Redis diff --git a/Plugins/SQLExportPlugin/Info.plist b/Plugins/SQLExportPlugin/Info.plist index 279c69fe5a..fd6dcc5ec1 100644 --- a/Plugins/SQLExportPlugin/Info.plist +++ b/Plugins/SQLExportPlugin/Info.plist @@ -3,7 +3,7 @@ TableProPluginKitVersion - 39 + TABLEPRO_PLUGINKIT_VERSION TableProProvidesExportFormatIds sql diff --git a/Plugins/SQLImportPlugin/Info.plist b/Plugins/SQLImportPlugin/Info.plist index 369a47df4b..262e0c28d1 100644 --- a/Plugins/SQLImportPlugin/Info.plist +++ b/Plugins/SQLImportPlugin/Info.plist @@ -3,7 +3,7 @@ TableProPluginKitVersion - 39 + TABLEPRO_PLUGINKIT_VERSION TableProProvidesImportFormatIds sql diff --git a/Plugins/SQLiteDriverPlugin/Info.plist b/Plugins/SQLiteDriverPlugin/Info.plist index 56a956e9dd..5354fb9599 100644 --- a/Plugins/SQLiteDriverPlugin/Info.plist +++ b/Plugins/SQLiteDriverPlugin/Info.plist @@ -3,7 +3,7 @@ TableProPluginKitVersion - 39 + TABLEPRO_PLUGINKIT_VERSION TableProProvidesDatabaseTypeIds SQLite diff --git a/Plugins/SnowflakeDriverPlugin/Info.plist b/Plugins/SnowflakeDriverPlugin/Info.plist index 88fdb3660c..ad014bef07 100644 --- a/Plugins/SnowflakeDriverPlugin/Info.plist +++ b/Plugins/SnowflakeDriverPlugin/Info.plist @@ -3,6 +3,6 @@ TableProPluginKitVersion - 39 + TABLEPRO_PLUGINKIT_VERSION diff --git a/Plugins/SpannerDriverPlugin/Info.plist b/Plugins/SpannerDriverPlugin/Info.plist index 316c35ad37..36fa241b70 100644 --- a/Plugins/SpannerDriverPlugin/Info.plist +++ b/Plugins/SpannerDriverPlugin/Info.plist @@ -3,7 +3,7 @@ TableProPluginKitVersion - 39 + TABLEPRO_PLUGINKIT_VERSION TableProProvidesDatabaseTypeIds Spanner diff --git a/Plugins/SurrealDBDriverPlugin/Info.plist b/Plugins/SurrealDBDriverPlugin/Info.plist index acb61d7be8..34c7f92701 100644 --- a/Plugins/SurrealDBDriverPlugin/Info.plist +++ b/Plugins/SurrealDBDriverPlugin/Info.plist @@ -3,7 +3,7 @@ TableProPluginKitVersion - 39 + TABLEPRO_PLUGINKIT_VERSION TableProProvidesDatabaseTypeIds SurrealDB diff --git a/Plugins/TeradataDriverPlugin/Info.plist b/Plugins/TeradataDriverPlugin/Info.plist index 88fdb3660c..ad014bef07 100644 --- a/Plugins/TeradataDriverPlugin/Info.plist +++ b/Plugins/TeradataDriverPlugin/Info.plist @@ -3,6 +3,6 @@ TableProPluginKitVersion - 39 + TABLEPRO_PLUGINKIT_VERSION diff --git a/Plugins/TrinoDriverPlugin/Info.plist b/Plugins/TrinoDriverPlugin/Info.plist index 88fdb3660c..ad014bef07 100644 --- a/Plugins/TrinoDriverPlugin/Info.plist +++ b/Plugins/TrinoDriverPlugin/Info.plist @@ -3,6 +3,6 @@ TableProPluginKitVersion - 39 + TABLEPRO_PLUGINKIT_VERSION diff --git a/Plugins/TypesenseDriverPlugin/Info.plist b/Plugins/TypesenseDriverPlugin/Info.plist index 4e0ab9eefb..0e0d2c4e6c 100644 --- a/Plugins/TypesenseDriverPlugin/Info.plist +++ b/Plugins/TypesenseDriverPlugin/Info.plist @@ -3,7 +3,7 @@ TableProPluginKitVersion - 39 + TABLEPRO_PLUGINKIT_VERSION TableProProvidesDatabaseTypeIds Typesense diff --git a/Plugins/WeaviateDriverPlugin/Info.plist b/Plugins/WeaviateDriverPlugin/Info.plist index 0ce6311681..153b6de197 100644 --- a/Plugins/WeaviateDriverPlugin/Info.plist +++ b/Plugins/WeaviateDriverPlugin/Info.plist @@ -3,7 +3,7 @@ TableProPluginKitVersion - 39 + TABLEPRO_PLUGINKIT_VERSION TableProProvidesDatabaseTypeIds Weaviate diff --git a/Plugins/XLSXExportPlugin/Info.plist b/Plugins/XLSXExportPlugin/Info.plist index 1f676a78e2..661aa2ce9e 100644 --- a/Plugins/XLSXExportPlugin/Info.plist +++ b/Plugins/XLSXExportPlugin/Info.plist @@ -3,7 +3,7 @@ TableProPluginKitVersion - 39 + TABLEPRO_PLUGINKIT_VERSION TableProProvidesExportFormatIds xlsx diff --git a/Plugins/XLSXImportPlugin/Info.plist b/Plugins/XLSXImportPlugin/Info.plist index 5407e53283..a87e8e8ac3 100644 --- a/Plugins/XLSXImportPlugin/Info.plist +++ b/Plugins/XLSXImportPlugin/Info.plist @@ -3,7 +3,7 @@ TableProPluginKitVersion - 39 + TABLEPRO_PLUGINKIT_VERSION TableProProvidesImportFormatIds xlsx diff --git a/TablePro/Core/Plugins/PluginManager.swift b/TablePro/Core/Plugins/PluginManager.swift index 7d00d75e10..4f8586d891 100644 --- a/TablePro/Core/Plugins/PluginManager.swift +++ b/TablePro/Core/Plugins/PluginManager.swift @@ -9,9 +9,19 @@ import TableProPluginKit @MainActor final class PluginManager: ObservableObject { static let shared = PluginManager(userDefaults: AppStorageEnvironment.shared.defaults) - nonisolated static let currentPluginKitVersion = 39 + nonisolated static let currentPluginKitVersion = pluginKitVersion(infoKey: "TableProPluginKitVersion") - nonisolated static let minimumCompatiblePluginKitVersion = 19 + nonisolated static let minimumCompatiblePluginKitVersion = pluginKitVersion( + infoKey: "TableProPluginKitMinimumVersion" + ) + + nonisolated private static func pluginKitVersion(infoKey: String) -> Int { + guard let value = Bundle.main.object(forInfoDictionaryKey: infoKey) as? String, + let version = Int(value) else { + preconditionFailure("Info.plist has no integer \(infoKey); Configs/PluginKit.xcconfig sets it") + } + return version + } private let builtInPluginsURL: URL? internal let userPluginsDir: URL diff --git a/TablePro/Info.plist b/TablePro/Info.plist index 376238ace1..d34f4b71b4 100644 --- a/TablePro/Info.plist +++ b/TablePro/Info.plist @@ -20,6 +20,10 @@ 86400 SUAutomaticallyUpdate + TableProPluginKitVersion + $(TABLEPRO_PLUGINKIT_VERSION) + TableProPluginKitMinimumVersion + $(TABLEPRO_PLUGINKIT_MINIMUM_VERSION) CFBundleDocumentTypes diff --git a/TableProTests/Core/Plugins/BundledPluginStampTests.swift b/TableProTests/Core/Plugins/BundledPluginStampTests.swift new file mode 100644 index 0000000000..77dbc8b7cd --- /dev/null +++ b/TableProTests/Core/Plugins/BundledPluginStampTests.swift @@ -0,0 +1,41 @@ +import Foundation +@testable import TablePro +import Testing + +struct BundledPluginStampTests { + private struct StampedPlugin { + let name: String + let info: [String: Any] + } + + private static func bundledPlugins() throws -> [StampedPlugin] { + let plugIns = try #require(Bundle.main.builtInPlugInsURL) + let bundles = try FileManager.default.contentsOfDirectory(at: plugIns, includingPropertiesForKeys: nil) + .filter { $0.pathExtension == "tableplugin" } + return try bundles.map { bundle in + let data = try Data(contentsOf: bundle.appendingPathComponent("Contents/Info.plist")) + let plist = try PropertyListSerialization.propertyList(from: data, format: nil) + let info = try #require(plist as? [String: Any], "\(bundle.lastPathComponent) has no Info.plist dictionary") + return StampedPlugin(name: bundle.lastPathComponent, info: info) + } + } + + @Test("Every bundled plugin declares the app's PluginKit version as an integer") + func bundledPluginsCarryTheAppKit() throws { + let plugins = try Self.bundledPlugins() + #expect(!plugins.isEmpty) + for plugin in plugins { + let declared = plugin.info["TableProPluginKitVersion"] + #expect( + declared as? Int == PluginManager.currentPluginKitVersion, + "\(plugin.name) declares \(String(describing: declared)), the app is \(PluginManager.currentPluginKitVersion)" + ) + } + } + + @Test("The app's PluginKit range is stamped from the build setting") + func appKitRangeIsOrdered() { + #expect(PluginManager.minimumCompatiblePluginKitVersion > 0) + #expect(PluginManager.minimumCompatiblePluginKitVersion <= PluginManager.currentPluginKitVersion) + } +} diff --git a/project.yml b/project.yml index fd3efe4dfc..bc75aa5a70 100644 --- a/project.yml +++ b/project.yml @@ -96,9 +96,14 @@ targetTemplates: base: COMBINE_HIDPI_IMAGES: YES CURRENT_PROJECT_VERSION: 1 + DEVELOPMENT_TEAM: $(TABLEPRO_DEVELOPMENT_TEAM) + ENABLE_HARDENED_RUNTIME: YES GENERATE_INFOPLIST_FILE: YES INFOPLIST_FILE: Plugins/${folder}/Info.plist INFOPLIST_KEY_NSPrincipalClass: $(PRODUCT_MODULE_NAME).${principalClass} + # Keeps TableProPluginKitVersion an : .claude/rules/plugin-system.md, ABI. + INFOPLIST_PREPROCESS: YES + INFOPLIST_PREPROCESSOR_DEFINITIONS: TABLEPRO_PLUGINKIT_VERSION=$(TABLEPRO_PLUGINKIT_VERSION) LD_RUNPATH_SEARCH_PATHS: - $(inherited) - "@executable_path/../Frameworks" @@ -115,6 +120,9 @@ targetTemplates: SWIFT_APPROACHABLE_CONCURRENCY: YES SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY: YES WRAPPER_EXTENSION: tableplugin + configs: + Release: + DEPLOYMENT_POSTPROCESSING: YES # A bundled plugin's folder compiled again with its tests and no host app. It links the same # TableProPluginKit framework the plugin does: a package product that depended on the package's @@ -134,6 +142,8 @@ targetTemplates: settings: base: GENERATE_INFOPLIST_FILE: YES + # Set, or XcodeGen picks the plugin folder's Info.plist, which only the plugin preprocesses. + INFOPLIST_FILE: "" PRODUCT_BUNDLE_IDENTIFIER: com.TablePro.${target_name} STRING_CATALOG_GENERATE_SYMBOLS: NO SWIFT_APPROACHABLE_CONCURRENCY: YES @@ -841,6 +851,8 @@ targets: settings: base: GENERATE_INFOPLIST_FILE: YES + # Set for the reason the PluginTests template gives. + INFOPLIST_FILE: "" PRODUCT_BUNDLE_IDENTIFIER: com.TablePro.FormatPluginTests STRING_CATALOG_GENERATE_SYMBOLS: NO SWIFT_APPROACHABLE_CONCURRENCY: YES diff --git a/scripts/ci/check-pluginkit-bump-cadence.py b/scripts/ci/check-pluginkit-bump-cadence.py index 335a7d141b..c05a33d55c 100644 --- a/scripts/ci/check-pluginkit-bump-cadence.py +++ b/scripts/ci/check-pluginkit-bump-cadence.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -"""Fail when currentPluginKitVersion has moved more than one past the newest release tag. +"""Fail when TABLEPRO_PLUGINKIT_VERSION has moved more than one past the newest release tag. Only the value at release time ever ships, so every PluginKit change in one release cycle reuses the number the first one took. The registry keeps binaries for the three newest kit versions, and @@ -10,7 +10,8 @@ The newest release is the highest vX.Y.Z tag by version number. With --remote the tag list comes from that remote and the one tag is fetched when it is missing, so a shallow CI checkout with no tags works. The fetch is at depth 1 only in a checkout that is already shallow: in a full clone it would -cut the history every worktree shares down to that one commit. +cut the history every worktree shares down to that one commit. A tag cut before +Configs/PluginKit.xcconfig existed declares the kit in PluginManager.swift. test_check_pluginkit_bump_cadence.py pins what it must and must not report. """ @@ -23,8 +24,10 @@ from pathlib import Path ROOT = Path(__file__).resolve().parents[2] -PLUGIN_MANAGER = "TablePro/Core/Plugins/PluginManager.swift" -KIT_DECLARATION = re.compile(r"\bstatic\s+let\s+currentPluginKitVersion\s*=\s*(\d+)\b") +KIT_CONFIG = "Configs/PluginKit.xcconfig" +KIT_SETTING = re.compile(r"^TABLEPRO_PLUGINKIT_VERSION\s*=\s*(\d+)\s*$", re.MULTILINE) +LEGACY_PLUGIN_MANAGER = "TablePro/Core/Plugins/PluginManager.swift" +LEGACY_DECLARATION = re.compile(r"\bstatic\s+let\s+currentPluginKitVersion\s*=\s*(\d+)\b") RELEASE_TAG = re.compile(r"^v(\d+)\.(\d+)\.(\d+)$") @@ -35,13 +38,20 @@ def git(repo: Path, *args: str) -> str: return result.stdout -def kit_version(source: str, origin: str) -> int: - match = KIT_DECLARATION.search(source) +def kit_version(source: str, origin: str, pattern: re.Pattern[str] = KIT_SETTING) -> int: + match = pattern.search(source) if match is None: - raise SystemExit(f"ERROR: no currentPluginKitVersion declaration in {origin}") + raise SystemExit(f"ERROR: no PluginKit version in {origin}") return int(match.group(1)) +def released_kit_version(repo: Path, tag: str) -> int: + for path, pattern in ((KIT_CONFIG, KIT_SETTING), (LEGACY_PLUGIN_MANAGER, LEGACY_DECLARATION)): + if has_object(repo, f"{tag}:{path}"): + return kit_version(git(repo, "show", f"{tag}:{path}"), f"{tag}:{path}", pattern) + raise SystemExit(f"ERROR: {tag} has neither {KIT_CONFIG} nor {LEGACY_PLUGIN_MANAGER}") + + def newest_release_tag(names: list[str]) -> str | None: releases = [ (tuple(int(part) for part in match.groups()), name) @@ -60,16 +70,16 @@ def remote_tag_names(repo: Path, remote: str) -> list[str]: return [line.split("refs/tags/", 1)[1] for line in output.splitlines() if "refs/tags/" in line] -def has_commit(repo: Path, ref: str) -> bool: +def has_object(repo: Path, ref: str) -> bool: result = subprocess.run( - ["git", "-C", str(repo), "rev-parse", "--quiet", "--verify", f"{ref}^{{commit}}"], + ["git", "-C", str(repo), "rev-parse", "--quiet", "--verify", ref], capture_output=True, ) return result.returncode == 0 def fetch_tag(repo: Path, remote: str, tag: str) -> None: - if has_commit(repo, f"refs/tags/{tag}"): + if has_object(repo, f"refs/tags/{tag}^{{commit}}"): return shallow = git(repo, "rev-parse", "--is-shallow-repository").strip() == "true" depth = ["--depth=1"] if shallow else [] @@ -80,7 +90,7 @@ def cadence_violation(tag: str, released: int, current: int) -> str | None: if current <= released + 1: return None return ( - f"currentPluginKitVersion is {current}, but {tag} shipped {released}. It moves at most once " + f"TABLEPRO_PLUGINKIT_VERSION is {current}, but {tag} shipped {released}. It moves at most once " f"per release cycle, so every PluginKit change until the next release reuses {released + 1}." ) @@ -94,19 +104,18 @@ def main(argv: list[str] | None = None) -> int: names = remote_tag_names(args.repo, args.remote) if args.remote else local_tag_names(args.repo) tag = newest_release_tag(names) if tag is None: - raise SystemExit("ERROR: no vX.Y.Z release tag to compare currentPluginKitVersion against") + raise SystemExit("ERROR: no vX.Y.Z release tag to compare TABLEPRO_PLUGINKIT_VERSION against") if args.remote: fetch_tag(args.repo, args.remote, tag) - released = kit_version(git(args.repo, "show", f"{tag}:{PLUGIN_MANAGER}"), f"{tag}:{PLUGIN_MANAGER}") - current_path = args.repo / PLUGIN_MANAGER - current = kit_version(current_path.read_text(encoding="utf-8"), PLUGIN_MANAGER) + released = released_kit_version(args.repo, tag) + current = kit_version((args.repo / KIT_CONFIG).read_text(encoding="utf-8"), KIT_CONFIG) violation = cadence_violation(tag, released, current) if violation: print(violation, file=sys.stderr) return 1 - print(f"currentPluginKitVersion {current}; {tag} shipped {released}.") + print(f"TABLEPRO_PLUGINKIT_VERSION {current}; {tag} shipped {released}.") return 0 diff --git a/scripts/ci/test_check_pluginkit_bump_cadence.py b/scripts/ci/test_check_pluginkit_bump_cadence.py index dc64c7f80b..add50f8b36 100644 --- a/scripts/ci/test_check_pluginkit_bump_cadence.py +++ b/scripts/ci/test_check_pluginkit_bump_cadence.py @@ -11,7 +11,8 @@ from pathlib import Path SCRIPT = Path(__file__).with_name("check-pluginkit-bump-cadence.py") -PLUGIN_MANAGER = Path("TablePro/Core/Plugins/PluginManager.swift") +KIT_CONFIG = Path("Configs/PluginKit.xcconfig") +LEGACY_PLUGIN_MANAGER = Path("TablePro/Core/Plugins/PluginManager.swift") GIT_ENV = { **os.environ, @@ -24,7 +25,11 @@ } -def plugin_manager_source(kit: int) -> str: +def kit_config_source(kit: int) -> str: + return f"TABLEPRO_PLUGINKIT_VERSION = {kit}\nTABLEPRO_PLUGINKIT_MINIMUM_VERSION = 19\n" + + +def legacy_plugin_manager_source(kit: int) -> str: return ( "final class PluginManager {\n" f" nonisolated static let currentPluginKitVersion = {kit}\n" @@ -46,14 +51,21 @@ def git(self, *args: str) -> str: text=True, ).stdout - def write_kit(self, kit: int) -> None: - target = self.path / PLUGIN_MANAGER + def write(self, relative: Path, text: str) -> None: + target = self.path / relative target.parent.mkdir(parents=True, exist_ok=True) - target.write_text(plugin_manager_source(kit), encoding="utf-8") + target.write_text(text, encoding="utf-8") - def commit_kit(self, kit: int, tag: str | None = None) -> None: - self.write_kit(kit) - self.git("add", str(PLUGIN_MANAGER)) + def write_kit(self, kit: int) -> None: + self.write(KIT_CONFIG, kit_config_source(kit)) + + def commit_kit(self, kit: int, tag: str | None = None, legacy: bool = False) -> None: + if legacy: + self.write(LEGACY_PLUGIN_MANAGER, legacy_plugin_manager_source(kit)) + self.git("add", str(LEGACY_PLUGIN_MANAGER)) + else: + self.write_kit(kit) + self.git("add", str(KIT_CONFIG)) self.git("commit", "--quiet", "--allow-empty", "-m", f"kit {kit}") if tag: self.git("tag", tag) @@ -126,8 +138,28 @@ def test_a_repository_without_a_release_tag_fails(self) -> None: def test_a_missing_declaration_fails(self) -> None: self.repo.commit_kit(30, tag="v0.74.0") - (self.repo.path / PLUGIN_MANAGER).write_text("final class PluginManager {}\n", encoding="utf-8") - self.assertFails(self.repo.run_check(), "currentPluginKitVersion") + self.repo.write(KIT_CONFIG, "TABLEPRO_PLUGINKIT_MINIMUM_VERSION = 19\n") + self.assertFails(self.repo.run_check(), str(KIT_CONFIG)) + + def test_a_release_before_the_xcconfig_is_read_from_swift(self) -> None: + self.repo.commit_kit(39, tag="v0.81.0", legacy=True) + self.repo.commit_kit(40) + self.assertPasses(self.repo.run_check()) + + self.repo.write_kit(41) + self.assertFails(self.repo.run_check(), "41", "v0.81.0", "39") + + def test_the_xcconfig_wins_over_a_leftover_swift_literal(self) -> None: + self.repo.commit_kit(30, legacy=True) + self.repo.commit_kit(39, tag="v0.82.0") + self.repo.write_kit(40) + self.assertPasses(self.repo.run_check()) + + def test_a_release_with_no_kit_fails(self) -> None: + self.repo.git("commit", "--quiet", "--allow-empty", "-m", "empty") + self.repo.git("tag", "v0.74.0") + self.repo.write_kit(30) + self.assertFails(self.repo.run_check(), "v0.74.0", str(KIT_CONFIG)) def test_a_shallow_clone_reads_the_release_from_its_remote(self) -> None: self.repo.commit_kit(25, tag="v0.73.0") diff --git a/scripts/release-plugin-for-shipped-app.sh b/scripts/release-plugin-for-shipped-app.sh index 7290e0d258..930b97c849 100755 --- a/scripts/release-plugin-for-shipped-app.sh +++ b/scripts/release-plugin-for-shipped-app.sh @@ -66,10 +66,16 @@ if ! git -C "$REPO_ROOT" rev-parse --verify "$APP_TAG^{commit}" >/dev/null 2>&1; exit 1 fi -KIT=$(git -C "$REPO_ROOT" show "$APP_TAG:TablePro/Core/Plugins/PluginManager.swift" \ - | sed -n 's/.*currentPluginKitVersion = \([0-9]*\).*/\1/p' | head -1) +if git -C "$REPO_ROOT" cat-file -e "$APP_TAG:Configs/PluginKit.xcconfig" 2>/dev/null; then + KIT=$(git -C "$REPO_ROOT" show "$APP_TAG:Configs/PluginKit.xcconfig" \ + | sed -n 's/^TABLEPRO_PLUGINKIT_VERSION *= *\([0-9][0-9]*\) *$/\1/p') +else + # A tag cut before Configs/PluginKit.xcconfig existed declares the kit in Swift. + KIT=$(git -C "$REPO_ROOT" show "$APP_TAG:TablePro/Core/Plugins/PluginManager.swift" \ + | sed -n 's/.*currentPluginKitVersion = \([0-9]*\).*/\1/p' | head -1) +fi if [ -z "$KIT" ]; then - echo "error: could not read currentPluginKitVersion out of $APP_TAG" >&2 + echo "error: could not read the PluginKit version out of $APP_TAG" >&2 exit 1 fi