Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .claude/rules/ci.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ The required check names are in `AGENTS.md` (CI). Reading a red check: `.claude/
- Setup, in order: checkout, setup-xcode, prepare-build (setup-xcodegen, `scripts/download-libs.sh`, `generate-project.sh macos`, SPM restore), resolve, SPM save.
- One `build-for-testing` with no `-testPlan` builds both plans and writes one `.xctestrun` per plan, with signing, the index store and the compilation cache off.
- `COMPILE_PLUGINS` is true when the `plugins` detector is not false, on a PR and a push alike; a dispatch has no base, so it always compiles. It adds Go, the Dameng and HANA bridge builds, `xcodebuild build -scheme AllPlugins`, then `scripts/ci/verify-plugin-loads.sh`.
- With `COMPILE_PLUGINS`, `scripts/check-pluginkit-abi.sh` then checks PluginKit against the newest `v*` tag, `--frozen` on a PR into `release/*`; for that tag the job checks out with `fetch-depth: 0` and `filter: blob:none`.
- `test-without-building -testPlan Unit`, then `-testPlan UI`. The UI plan runs after a Unit failure too, so one round trip reports both.
- No shards, and no parallel, iteration or retry flags: the plans decide.
- `scripts/ci/summarize-xcresult.sh` always runs. Evidence uploads only on failure (`.claude/skills/fix-issue/references/verification-apple.md`, Reading CI).
Expand Down Expand Up @@ -105,7 +106,6 @@ The required check names are in `AGENTS.md` (CI). Reading a red check: `.claude/
- `check-lib-sourcing.py`, `check-plugin-manifest.py`, `check-plugin-provides.py`, `check-deployment-target.sh`, `check-ios-shared-isolation.py`;
- `localization.py plugins` and `plugins --target ios`;
- `check-string-catalogs.py`, `check-license-inventory.py`, `check-entitlements.py`, `check-test-plans.py`;
- `check-pluginkit-bump-cadence.py`;
- `scripts/audit-refactor-health.sh --check`, `check-hub-ratchet.py`;
- the Go checks on the HANA helper, `govulncheck` among them, when the `hana` detector is true. New advisories between changes arrive as Dependabot alerts.
- `audit-refactor-health.sh --check` fails a file defining a production `DatabaseType` outside `Packages/TableProCore/Sources/TableProCoreTypes/DatabaseType.swift`, unless `.github/duplicate-contract-baseline.txt` lists its path. Moving a listed file moves its baseline path in the same commit. Never add an entry to let a new copy pass.
Expand Down
15 changes: 4 additions & 11 deletions .claude/rules/plugin-system.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,20 +24,13 @@ 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 `TABLEPRO_PLUGINKIT_VERSION` in `Configs/PluginKit.xcconfig`, at most one past the newest `vX.Y.Z` tag: later changes reuse the pending number. `scripts/ci/check-pluginkit-bump-cadence.py` checks it in Repo Hygiene. A `release/*` line never bumps it.
- Every ABI change sets `TABLEPRO_PLUGINKIT_VERSION` in `Configs/PluginKit.xcconfig` to one past the kit of the newest `v*` tag reachable from HEAD; later changes before the next tag reuse it. A `release/*` line never bumps it.
- The build stamps that value into `TableProPluginKitVersion` of the app and of every plugin. A plugin's `Info.plist` holds `<integer>TABLEPRO_PLUGINKIT_VERSION</integer>` and is preprocessed, because every shipped app reads the key with `as? Int`; the source file therefore fails `plutil -lint`.
- After an additive change, 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 `TABLEPRO_PLUGINKIT_MINIMUM_VERSION`. Every tag ships every registry plugin built against its own kit, so the release that carries the change carries plugins in its range.
- Check with `verify.sh abi <merge-base>` (`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:

```bash
L=<the log verify.sh abi printed>
comm -23 <(grep -E '^-[^-]' "$L" | sed 's/^-[[:space:]]*//' | sort -u) \
<(grep -E '^\+[^+]' "$L" | sed 's/^+[[:space:]]*//' | sort -u)
```

- `scripts/check-pluginkit-abi.sh [--frozen] [base]` (`verify.sh abi [base]`) builds PluginKit at the base, by default the newest `v*` tag reachable from HEAD, and at HEAD with Xcode's ABI checker (`RUN_SWIFT_ABI_CHECKER_TOOL`). macOS Tests runs it whenever it compiles the plugins, with `--frozen` on a PR into `release/*`.
- A checker error is a breaking change: the kit and the minimum must both be one past the base's kit, so the host refuses every plugin built before it. A baseline that changed with no error needs the kit one past the base's; an identical one needs the base's kit. `--frozen` fails on any change.
- The checker reports breakage only; an additive change shows as a changed baseline. Its errors are in the log `verify.sh abi` prints.
- Mark a public enum `@frozen` only when its case set is closed. `PluginCapability` and the transfer structs stay non-frozen so they can grow.
- PluginKit code with no callers stays: removing it is an ABI change.

Expand Down
9 changes: 5 additions & 4 deletions .claude/skills/fix-issue/scripts/lib/platform-apple.sh
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
# verify.sh [options] uitest [Test...] # TestPlans/UI.xctestplan, all of it when no Test is named
# verify.sh [options] package <Package> [regex] # swift test in Packages/<Package>
# verify.sh [options] ios <Suite> [Suite...] # TableProMobileTests on a simulator
# verify.sh [options] abi <merge-base>
# verify.sh [options] abi [base] # PluginKit ABI checker; default base: the newest v* tag
# verify.sh [options] lint <path> [path...]
# verify.sh [options] docs [docs-checkout] # TableProApp/docs checks against this tree (default ../tablepro-docs)
# verify.sh [options] l10n # plugin and package strings in the mac and ios catalogs
Expand Down Expand Up @@ -497,12 +497,13 @@ platform_step() {
;;

abi)
[ $# -ge 1 ] || usage
STEP_DETAIL="merge-base $1"
[ $# -le 1 ] || usage
STEP_DETAIL="base ${1:-newest v* tag}"
setup_toolchain
require_tool xcodebuild
require_tool xcodegen
log="$(new_log abi)"
run_logged "$log" "$REPO_ROOT/scripts/check-pluginkit-abi.sh" "$1"
run_logged "$log" "$REPO_ROOT/scripts/check-pluginkit-abi.sh" "$@"
code=$?
if [ $code -eq 0 ]; then
STATUS=PASS
Expand Down
15 changes: 15 additions & 0 deletions .github/workflows/macos-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,11 @@ jobs:
NSUnbufferedIO: "YES"
COMPILE_PLUGINS: ${{ needs.changes.outputs.plugins != 'false' }}
steps:
# The PluginKit ABI step builds the newest v* tag, so it needs the tags and their history.
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
filter: blob:none

- uses: ./.github/actions/setup-xcode

Expand Down Expand Up @@ -113,6 +117,17 @@ jobs:
| xcbeautify --renderer github-actions
scripts/ci/verify-plugin-loads.sh DerivedData/Build/Products/Debug

- name: Check the PluginKit ABI
if: ${{ !cancelled() && env.COMPILE_PLUGINS == 'true' }}
env:
BASE_REF: ${{ github.base_ref }}
run: |
if [[ "$BASE_REF" == release/* ]]; then
scripts/check-pluginkit-abi.sh --frozen
else
scripts/check-pluginkit-abi.sh
fi

- name: Run the Unit test plan
if: ${{ !cancelled() && steps.build.outcome == 'success' }}
run: |
Expand Down
3 changes: 0 additions & 3 deletions .github/workflows/repo-hygiene.yml
Original file line number Diff line number Diff line change
Expand Up @@ -120,9 +120,6 @@ jobs:
- name: Check every test target is in its test plan
run: python3 scripts/ci/check-test-plans.py

- name: Check currentPluginKitVersion moved at most once since the last release
run: python3 scripts/ci/check-pluginkit-bump-cadence.py --remote origin

- name: Check the shared-contract drift gates
run: scripts/audit-refactor-health.sh --check

Expand Down
13 changes: 0 additions & 13 deletions TableProTests/Core/Plugins/PluginKitABIResilienceTests.swift
Original file line number Diff line number Diff line change
@@ -1,16 +1,3 @@
//
// PluginKitABIResilienceTests.swift
// TableProTests
//
// Guards the additive-safety promise of the resilient PluginKit ABI: a driver
// that implements only the required surface and omits every defaulted
// requirement must still satisfy the protocol, and each default must return its
// documented value. If a requirement is ever added without a default, this file
// (and FakeMSSQLPluginDriver) stops compiling, flagging a breaking change that
// needs a currentPluginKitVersion bump. Cross-binary load compatibility itself
// is checked by scripts/check-pluginkit-abi.sh, run manually before merging.
//

import Foundation
@testable import TablePro
import TableProPluginKit
Expand Down
177 changes: 94 additions & 83 deletions scripts/check-pluginkit-abi.sh
Original file line number Diff line number Diff line change
@@ -1,100 +1,111 @@
#!/usr/bin/env bash
set -euo pipefail

# PluginKit ABI gate (toolchain-independent).
#
# Builds TableProPluginKit at the current tree AND at a base ref with the SAME toolchain, then
# diffs their public Swift interfaces. Comparing two builds from one compiler means Swift version
# drift between a dev machine and CI cannot produce a false diff, so there is no committed baseline
# to keep in sync. A reported diff is a real ABI change; the rules printed at the end say what to
# raise in Configs/PluginKit.xcconfig.
#
# Both sides are generated from their project.yml first, so this regenerates
# TablePro.xcodeproj in the working tree as well as in the base checkout.
#
# Usage: scripts/check-pluginkit-abi.sh [base-ref] (default: origin/main)
# Usage: scripts/check-pluginkit-abi.sh [--frozen] [base]
# Rules: .claude/rules/plugin-system.md, ABI.

PROJECT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
BASE_REF="${1:-origin/main}"

RESULT=""

# Build TableProPluginKit in project dir $1, writing its normalized public interface to $2.
# Sets RESULT to ok | none (no interface emitted, i.e. Library Evolution off) | failed.
# The .swiftinterface lives under an arch-named subdir (arm64-apple-macos on Apple Silicon,
# x86_64-apple-macos on Intel), so locate it by glob instead of hardcoding the host arch.
build_interface() {
local dir="$1" out="$2" sym interface
sym="$(mktemp -d)"
if ! (cd "$dir" && xcodegen generate --quiet --spec project.yml) >"$sym/generate.log" 2>&1; then
RESULT="failed"
tail -20 "$sym/generate.log"
return
fi
if ! xcodebuild -project "$dir/TablePro.xcodeproj" -target TableProPluginKit -configuration Debug \
-skipPackagePluginValidation build SYMROOT="$sym" >"$sym/build.log" 2>&1; then
RESULT="failed"
tail -20 "$sym/build.log"
return
fi
interface="$(find "$sym/Debug/TableProPluginKit.framework" -name '*.swiftinterface' 2>/dev/null | head -1)"
if [ -n "$interface" ]; then
grep -v '^// swift-' "$interface" > "$out"
RESULT="ok"
else
RESULT="none"
fi
}
root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
frozen=0
if [ "${1:-}" = "--frozen" ]; then
frozen=1
shift
fi
if [ $# -gt 1 ]; then
echo "usage: scripts/check-pluginkit-abi.sh [--frozen] [base]" >&2
exit 2
fi
base="${1:-$(git -C "$root" describe --tags --abbrev=0 --match 'v[0-9]*')}"

if ! git -C "$PROJECT_DIR" diff --quiet HEAD; then
echo "::error::Working tree has uncommitted changes; commit or stash before running the ABI gate."
fail() {
echo "::error::$1"
exit 1
fi
}

base_sha="$(git -C "$PROJECT_DIR" rev-parse --verify "$BASE_REF" 2>/dev/null)" || {
echo "::error::cannot resolve base ref '$BASE_REF'"; exit 1
kits() {
awk -F' *= *' -v origin="$1" '
$1 == "TABLEPRO_PLUGINKIT_VERSION" { kit = $2 }
$1 == "TABLEPRO_PLUGINKIT_MINIMUM_VERSION" { minimum = $2 }
END {
if (kit !~ /^[0-9]+$/ || minimum !~ /^[0-9]+$/) {
print "::error::no PluginKit versions in " origin
exit 1
}
print kit, minimum
}'
}

base_kits="$(git -C "$root" show "$base:Configs/PluginKit.xcconfig" | kits "$base")"
head_kits="$(kits HEAD < "$root/Configs/PluginKit.xcconfig")"
read -r base_kit _ <<< "$base_kits"
read -r head_kit head_minimum <<< "$head_kits"

work="$(mktemp -d)"
trap 'rm -rf "$work"' EXIT
cleanup() {
git -C "$root" worktree remove --force "$work/src" > /dev/null 2>&1 || true
rm -rf "$work"
}
trap cleanup EXIT

echo "Building current TableProPluginKit..."
build_interface "$PROJECT_DIR" "$work/head.txt"
[ "$RESULT" = "failed" ] && { echo "::error::current TableProPluginKit build failed"; exit 1; }
head_result="$RESULT"
# Without -error-on-abi-breakage, swift-api-digester reports a breakage as a warning.
build_kit() {
local project="$1" side="$2"
shift 2
xcodebuild build -project "$project/TablePro.xcodeproj" -scheme TableProPluginKit -configuration Debug \
-destination platform=macOS -derivedDataPath "$work/$side/DerivedData" \
-clonedSourcePackagesDirPath "$work/packages" -skipPackagePluginValidation \
CODE_SIGNING_ALLOWED=NO OTHER_SWIFT_ABI_CHECKER_FLAGS=-error-on-abi-breakage \
RUN_SWIFT_ABI_GENERATION_TOOL=YES SWIFT_ABI_GENERATION_TOOL_OUTPUT_DIR="$work/$side/ABI" \
"$@" > "$work/$side.log" 2>&1
}

echo "Building base ($BASE_REF -> ${base_sha:0:8})..."
git clone --quiet --local --no-hardlinks "$PROJECT_DIR" "$work/base"
git -C "$work/base" checkout --quiet "$base_sha"
build_interface "$work/base" "$work/base.txt"
[ "$RESULT" = "failed" ] && { echo "::error::base TableProPluginKit build failed at $BASE_REF"; exit 1; }
base_result="$RESULT"
echo "Building TableProPluginKit at $base"
git -C "$root" worktree add --quiet --detach "$work/src" "$base"
(cd "$work/src" && xcodegen generate --quiet --spec project.yml)
build_kit "$work/src" base || { tail -30 "$work/base.log"; fail "TableProPluginKit does not build at $base"; }

if [ "$base_result" = "none" ]; then
echo "Base has no resilient interface (Library Evolution not enabled there). Bootstrap, nothing to compare. Pass."
exit 0
fi
echo "Building TableProPluginKit at HEAD"
build_kit "$root" head || { tail -30 "$work/head.log"; fail "TableProPluginKit does not build at HEAD"; }
for side in base head; do
ls "$work/$side/ABI"/*.json > /dev/null 2>&1 || fail "The $side build wrote no ABI baseline."
done

if [ "$head_result" = "none" ]; then
echo "::error::current build produced no .swiftinterface but the base did. Was BUILD_LIBRARY_FOR_DISTRIBUTION turned off?"
exit 1
echo "Checking the ABI against $base"
change=unchanged
if ! build_kit "$root" head RUN_SWIFT_ABI_CHECKER_TOOL=YES SWIFT_ABI_CHECKER_BASELINE_DIR="$work/base"; then
breakages="$(grep ': error: ABI breakage: ' "$work/head.log" | sort -u)" \
|| { tail -30 "$work/head.log"; fail "The ABI checker failed without reporting a breakage."; }
echo "$breakages"
change=breaking
else
compared=0
diff -rq "$work/base/ABI" "$work/head/ABI" > /dev/null || compared=$?
case "$compared" in
0) ;;
1) change=changed ;;
*) fail "Could not compare the ABI baselines." ;;
esac
fi

if diff -u "$work/base.txt" "$work/head.txt"; then
echo "PluginKit ABI unchanged vs $BASE_REF."
exit 0
next=$((base_kit + 1))
config="Configs/PluginKit.xcconfig"
if [ "$frozen" -eq 1 ] && [ "$change" != unchanged ]; then
fail "PluginKit's ABI differs from $base, and a release line never changes it."
fi

cat <<'EOF'

TableProPluginKit public ABI changed vs base (diff above). Decide additive vs breaking:
Additive: a new requirement with a default, a reordering, or a field on a non-frozen transfer
struct. Raise TABLEPRO_PLUGINKIT_VERSION in Configs/PluginKit.xcconfig. Shipped plugins
keep loading; the bump makes an older app refuse a plugin rebuilt against this API
rather than failing its load.
Breaking: a changed or removed requirement, a requirement without a default, a case on a @frozen
enum, or a frozen type's layout. Raise TABLEPRO_PLUGINKIT_VERSION and
TABLEPRO_PLUGINKIT_MINIMUM_VERSION.
Registry plugins ship with the next app release, built against the new kit.
EOF
exit 1
case "$change" in
breaking)
if [ "$head_kit" -ne "$next" ] || [ "$head_minimum" -ne "$next" ]; then
fail "Breaking change since $base: set TABLEPRO_PLUGINKIT_VERSION and TABLEPRO_PLUGINKIT_MINIMUM_VERSION to $next in $config."
fi
;;
changed)
if [ "$head_kit" -ne "$next" ]; then
fail "PluginKit's ABI changed since $base: set TABLEPRO_PLUGINKIT_VERSION to $next in $config."
fi
;;
unchanged)
if [ "$head_kit" -ne "$base_kit" ]; then
fail "PluginKit's ABI matches $base: TABLEPRO_PLUGINKIT_VERSION stays $base_kit in $config."
fi
;;
esac
echo "PluginKit ABI against $base: $change; kit $head_kit, minimum $head_minimum."
Loading
Loading