Skip to content

Commit 174fd0c

Browse files
committed
Swift 6 mode + off-main batch import (crash fix) + CI parity overhaul
Swift 6 language mode (SWIFT_VERSION 6.0, all targets): - Pure data layers opted out of the default MainActor isolation with native `nonisolated`: Constants enums, JSON blob helpers, Color hex math, the whole Bible import parse layer (importers, rich-text/GBF/USFM helpers, value types). - Fresh importer instance per import (the shared XML importer instances with mutable parser state were a latent data race). - SE-0371 `isolated deinit` for MainActor teardown (audio timer, screen observers, command router tokens, video observers/security scope). - VersionedSchema identifiers are `let`; test suites annotated @mainactor. Batch Bible import crash (SIGABRT malloc_zone_error on Thread 3): - New BackgroundImportActor (@Modelactor) — batch imports run entirely off the main actor on its OWN serialized ModelContext; the view's context no longer crosses thread hops mid-import. - ImportService.importBible is nonisolated(nonsending) — runs on the caller's isolation, saves per BOOK (chunked) inside autoreleasepool, progress hops to the main actor via @mainactor @sendable handler. - BatchImportSheet: re-entry guard + per-run status reset (fixes the glitched reopened sheet); bibles route through the actor. CI/CD parity + modernization: - Runner macos-26 + Xcode 26.3 (17C529) — EXACT match with the dev Mac; the old macos-15/Xcode 16.0 pair ignored the project's Swift-6-era settings. - All actions on current majors (checkout v7, upload-artifact v7, download-artifact v8, configure-pages v6, upload-pages-artifact v5, deploy-pages v5, gh-release v3) — no more Node 20 deprecation warnings. - Unit tests are a real gate (removed `|| true`; -only-testing unit suite). - Uniform inside-out ad-hoc re-sign of Sparkle (XPCs, Autoupdate, Updater.app, framework) + the app, preserving entitlements, then strict codesign verify — fixes the DYLD "different Team IDs" launch crash on other Macs. - Pages config cleaned (dangling deleted-branch source removed).
1 parent 4d1debd commit 174fd0c

24 files changed

Lines changed: 270 additions & 198 deletions

.github/workflows/build-and-release.yml

Lines changed: 63 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -15,27 +15,34 @@ concurrency:
1515
env:
1616
SCHEME: TopPresenter
1717
PRODUCT_NAME: TopPresenter
18-
XCODE_VERSION: '16.0'
18+
# EXACT parity with the dev Mac: Xcode 26.3 (17C529) on the macos-26 arm64
19+
# image. Older Xcode (16.x) silently IGNORES the project's Swift 6.x settings
20+
# (SWIFT_DEFAULT_ACTOR_ISOLATION, approachable concurrency) — never build the
21+
# release with a different major Xcode than the one used for development.
22+
XCODE_VERSION: '26.3'
1923

2024
jobs:
2125
build:
2226
name: Build & Package
23-
runs-on: macos-15
24-
timeout-minutes: 30
27+
runs-on: macos-26
28+
timeout-minutes: 40
2529
outputs:
2630
version: ${{ steps.package.outputs.VERSION }}
2731
prerelease_tag: ${{ steps.package.outputs.PRERELEASE_TAG }}
2832
prerelease_version: ${{ steps.package.outputs.PRERELEASE_VERSION }}
2933

3034
steps:
3135
- name: Checkout
32-
uses: actions/checkout@v5
36+
uses: actions/checkout@v7
3337

3438
- name: Select Xcode
35-
run: sudo xcode-select -s /Applications/Xcode_${{ env.XCODE_VERSION }}.app/Contents/Developer
36-
37-
- name: Show Xcode version
38-
run: xcodebuild -version
39+
run: |
40+
sudo xcode-select -s "/Applications/Xcode_${XCODE_VERSION}.app/Contents/Developer" || {
41+
echo "::error::Xcode ${XCODE_VERSION} not on this image. Available:"
42+
ls /Applications | grep -i '^Xcode' || true
43+
exit 1
44+
}
45+
xcodebuild -version
3946
4047
- name: Resolve dependencies
4148
run: xcodebuild -resolvePackageDependencies -scheme "$SCHEME" -project "$PRODUCT_NAME.xcodeproj"
@@ -44,10 +51,8 @@ jobs:
4451
run: |
4552
# Monotonic CFBundleVersion so Sparkle can tell releases apart (the project's
4653
# CURRENT_PROJECT_VERSION is a fixed 1). Ad-hoc code signing ("-") so the app's
47-
# entitlements (sandbox + Sparkle XPC) actually apply and the in-app updater's
48-
# installer works — no Apple certs needed (Gatekeeper still asks for right-click
49-
# → Open, exactly as before). Swap "-" for a Developer ID identity + add
50-
# notarization later for silent updates; see UPDATES.md.
54+
# entitlements (sandbox + Sparkle XPC) apply with no Apple certs; the bundle is
55+
# uniformly RE-signed below so every nested binary carries the same identity.
5156
xcodebuild \
5257
-scheme "$SCHEME" \
5358
-project "$PRODUCT_NAME.xcodeproj" \
@@ -62,18 +67,46 @@ jobs:
6267
CURRENT_PROJECT_VERSION=${{ github.run_number }} \
6368
clean build
6469
65-
- name: Run Unit Tests
70+
- name: Run unit tests (gate — failures fail the build)
6671
run: |
6772
xcodebuild \
6873
-scheme "$SCHEME" \
6974
-project "$PRODUCT_NAME.xcodeproj" \
7075
-destination 'platform=macOS' \
71-
-resultBundlePath TestResults.xcresult \
76+
-only-testing:TopPresenterTests \
7277
CODE_SIGN_IDENTITY="-" \
7378
CODE_SIGNING_REQUIRED=NO \
7479
CODE_SIGNING_ALLOWED=NO \
7580
DEVELOPMENT_TEAM="" \
76-
test || true
81+
test
82+
83+
- name: Re-sign uniformly (ad-hoc, inside-out) & verify
84+
run: |
85+
# DYLD kills the app if the process and a mapped framework carry DIFFERENT
86+
# Team IDs ("code signature not valid ... different Team IDs"). Guarantee
87+
# uniformity: re-sign every nested Sparkle binary and then the app itself
88+
# with the SAME ad-hoc identity, inside-out. No --options runtime here:
89+
# hardened runtime + ad-hoc (team-less) signatures = library validation
90+
# rejects the frameworks; plain ad-hoc launches everywhere.
91+
APP="build/Build/Products/Release/$PRODUCT_NAME.app"
92+
SPK="$APP/Contents/Frameworks/Sparkle.framework"
93+
for target in \
94+
"$SPK/Versions/B/XPCServices/Downloader.xpc" \
95+
"$SPK/Versions/B/XPCServices/Installer.xpc" \
96+
"$SPK/Versions/B/Autoupdate" \
97+
"$SPK/Versions/B/Updater.app" \
98+
"$SPK"; do
99+
if [ -e "$target" ]; then
100+
codesign --force --sign - --preserve-metadata=entitlements "$target"
101+
fi
102+
done
103+
codesign --force --sign - --preserve-metadata=entitlements "$APP"
104+
echo "── verify ──"
105+
codesign --verify --deep --strict --verbose=2 "$APP"
106+
echo "── identities (must ALL be adhoc, no TeamIdentifier) ──"
107+
for bin in "$APP" "$SPK" "$SPK/Versions/B/Autoupdate" "$SPK/Versions/B/Updater.app"; do
108+
codesign -dv "$bin" 2>&1 | grep -E 'Signature|TeamIdentifier' | sed "s|^|$(basename "$bin"): |"
109+
done
77110
78111
- name: Package app
79112
id: package
@@ -113,7 +146,7 @@ jobs:
113146
"$GITHUB_WORKSPACE/$PRODUCT_NAME-$VERSION.dmg"
114147
115148
- name: Upload artifact
116-
uses: actions/upload-artifact@v5
149+
uses: actions/upload-artifact@v7
117150
with:
118151
name: TopPresenter-${{ env.VERSION }}
119152
path: |
@@ -126,7 +159,7 @@ jobs:
126159
name: Pre-release (Alpha)
127160
needs: build
128161
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
129-
runs-on: macos-15
162+
runs-on: macos-26
130163
permissions:
131164
contents: write
132165
pages: write # deploy the appcast to Pages (no gh-pages branch)
@@ -139,10 +172,10 @@ jobs:
139172

140173
steps:
141174
- name: Checkout (for appcast scripts)
142-
uses: actions/checkout@v5
175+
uses: actions/checkout@v7
143176

144177
- name: Download artifact
145-
uses: actions/download-artifact@v5
178+
uses: actions/download-artifact@v8
146179
with:
147180
pattern: TopPresenter-*
148181
merge-multiple: true
@@ -151,7 +184,7 @@ jobs:
151184
run: echo "SHORT_SHA=${GITHUB_SHA::7}" >> $GITHUB_ENV
152185

153186
- name: Create alpha pre-release
154-
uses: softprops/action-gh-release@v2
187+
uses: softprops/action-gh-release@v3
155188
with:
156189
tag_name: ${{ needs.build.outputs.prerelease_tag }}
157190
name: "TopPresenter ${{ needs.build.outputs.prerelease_version }}"
@@ -193,24 +226,24 @@ jobs:
193226
194227
- name: Setup Pages
195228
if: ${{ env.SPARKLE_PRIVATE_KEY != '' }}
196-
uses: actions/configure-pages@v5
229+
uses: actions/configure-pages@v6
197230

198231
- name: Upload appcast artifact
199232
if: ${{ env.SPARKLE_PRIVATE_KEY != '' }}
200-
uses: actions/upload-pages-artifact@v3
233+
uses: actions/upload-pages-artifact@v5
201234
with:
202235
path: site
203236

204237
- name: Deploy appcast to Pages
205238
if: ${{ env.SPARKLE_PRIVATE_KEY != '' }}
206-
uses: actions/deploy-pages@v4
239+
uses: actions/deploy-pages@v5
207240

208241
# Stable release on final version tags (v1.0.0, v1.1.0, etc.)
209242
release:
210243
name: Stable Release
211244
needs: build
212245
if: startsWith(github.ref, 'refs/tags/v') && !contains(github.ref_name, '-')
213-
runs-on: macos-15
246+
runs-on: macos-26
214247
permissions:
215248
contents: write
216249
pages: write # deploy the appcast to Pages (no gh-pages branch)
@@ -222,16 +255,16 @@ jobs:
222255

223256
steps:
224257
- name: Checkout (for appcast scripts)
225-
uses: actions/checkout@v5
258+
uses: actions/checkout@v7
226259

227260
- name: Download artifact
228-
uses: actions/download-artifact@v5
261+
uses: actions/download-artifact@v8
229262
with:
230263
pattern: TopPresenter-*
231264
merge-multiple: true
232265

233266
- name: Create stable release
234-
uses: softprops/action-gh-release@v2
267+
uses: softprops/action-gh-release@v3
235268
with:
236269
name: "TopPresenter ${{ github.ref_name }}"
237270
generate_release_notes: true
@@ -260,14 +293,14 @@ jobs:
260293
261294
- name: Setup Pages
262295
if: ${{ env.SPARKLE_PRIVATE_KEY != '' }}
263-
uses: actions/configure-pages@v5
296+
uses: actions/configure-pages@v6
264297

265298
- name: Upload appcast artifact
266299
if: ${{ env.SPARKLE_PRIVATE_KEY != '' }}
267-
uses: actions/upload-pages-artifact@v3
300+
uses: actions/upload-pages-artifact@v5
268301
with:
269302
path: site
270303

271304
- name: Deploy appcast to Pages
272305
if: ${{ env.SPARKLE_PRIVATE_KEY != '' }}
273-
uses: actions/deploy-pages@v4
306+
uses: actions/deploy-pages@v5

TopPresenter.xcodeproj/project.pbxproj

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -441,7 +441,7 @@
441441
SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor;
442442
SWIFT_EMIT_LOC_STRINGS = YES;
443443
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
444-
SWIFT_VERSION = 5.0;
444+
SWIFT_VERSION = 6.0;
445445
};
446446
name = Debug;
447447
};
@@ -490,7 +490,7 @@
490490
SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor;
491491
SWIFT_EMIT_LOC_STRINGS = YES;
492492
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
493-
SWIFT_VERSION = 5.0;
493+
SWIFT_VERSION = 6.0;
494494
};
495495
name = Release;
496496
};
@@ -510,7 +510,7 @@
510510
SWIFT_APPROACHABLE_CONCURRENCY = YES;
511511
SWIFT_EMIT_LOC_STRINGS = NO;
512512
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
513-
SWIFT_VERSION = 5.0;
513+
SWIFT_VERSION = 6.0;
514514
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/TopPresenter.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/TopPresenter";
515515
};
516516
name = Debug;
@@ -531,7 +531,7 @@
531531
SWIFT_APPROACHABLE_CONCURRENCY = YES;
532532
SWIFT_EMIT_LOC_STRINGS = NO;
533533
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
534-
SWIFT_VERSION = 5.0;
534+
SWIFT_VERSION = 6.0;
535535
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/TopPresenter.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/TopPresenter";
536536
};
537537
name = Release;
@@ -550,7 +550,7 @@
550550
SWIFT_APPROACHABLE_CONCURRENCY = YES;
551551
SWIFT_EMIT_LOC_STRINGS = NO;
552552
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
553-
SWIFT_VERSION = 5.0;
553+
SWIFT_VERSION = 6.0;
554554
TEST_TARGET_NAME = TopPresenter;
555555
};
556556
name = Debug;
@@ -569,7 +569,7 @@
569569
SWIFT_APPROACHABLE_CONCURRENCY = YES;
570570
SWIFT_EMIT_LOC_STRINGS = NO;
571571
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
572-
SWIFT_VERSION = 5.0;
572+
SWIFT_VERSION = 6.0;
573573
TEST_TARGET_NAME = TopPresenter;
574574
};
575575
name = Release;

TopPresenter/Core/Constants.swift

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,13 @@
88
import Foundation
99

1010
// MARK: - Window Identifiers
11-
enum WindowIdentifiers {
11+
nonisolated enum WindowIdentifiers {
1212
static let main = "main-control"
1313
static let presentation = "presentation-output"
1414
}
1515

1616
// MARK: - Supported File Types
17-
enum SupportedBibleFormat: String, CaseIterable, Identifiable {
17+
nonisolated enum SupportedBibleFormat: String, CaseIterable, Identifiable {
1818
case topPresenter = "toppresenter"
1919
case osisXML = "osis"
2020
case zefaniaXML = "zefania"
@@ -67,7 +67,7 @@ enum SupportedBibleFormat: String, CaseIterable, Identifiable {
6767
}
6868

6969
// MARK: - Supported Export Formats
70-
enum SupportedExportFormat: String, CaseIterable, Identifiable {
70+
nonisolated enum SupportedExportFormat: String, CaseIterable, Identifiable {
7171
case topPresenter = "toppresenter"
7272
case plainText = "plaintext"
7373
case csv = "csv"
@@ -100,7 +100,7 @@ enum SupportedExportFormat: String, CaseIterable, Identifiable {
100100
}
101101

102102
// MARK: - USFM Book Abbreviation Mapping
103-
enum USFMBookIDs {
103+
nonisolated enum USFMBookIDs {
104104
static let mapping: [String: (name: String, number: Int)] = [
105105
"GEN": ("Genesis", 1), "EXO": ("Exodus", 2), "LEV": ("Leviticus", 3),
106106
"NUM": ("Numbers", 4), "DEU": ("Deuteronomy", 5), "JOS": ("Joshua", 6),
@@ -129,7 +129,7 @@ enum USFMBookIDs {
129129
}
130130

131131
// MARK: - MySword/Unbound Book Number Mapping
132-
enum BibleBookNumbers {
132+
nonisolated enum BibleBookNumbers {
133133
/// Maps 1-based book number to (name, testament)
134134
static let mapping: [Int: (name: String, testament: String)] = {
135135
var map: [Int: (name: String, testament: String)] = [:]
@@ -143,7 +143,7 @@ enum BibleBookNumbers {
143143
}()
144144
}
145145

146-
enum SupportedSongFormat: String, CaseIterable, Identifiable {
146+
nonisolated enum SupportedSongFormat: String, CaseIterable, Identifiable {
147147
case topPresenterJSON = "toppresenter-song"
148148
case openSongXML = "opensong"
149149
case openLyricsXML = "openlyrics"
@@ -177,7 +177,7 @@ enum SupportedSongFormat: String, CaseIterable, Identifiable {
177177
}
178178

179179
// MARK: - Supported Song Export Formats
180-
enum SupportedSongExportFormat: String, CaseIterable, Identifiable {
180+
nonisolated enum SupportedSongExportFormat: String, CaseIterable, Identifiable {
181181
case topPresenter = "toppresenter"
182182
case openLyricsXML = "openlyrics"
183183
case plainText = "plaintext"
@@ -211,7 +211,7 @@ enum SupportedSongExportFormat: String, CaseIterable, Identifiable {
211211
}
212212

213213
// MARK: - Presentation Defaults
214-
enum PresentationDefaults {
214+
nonisolated enum PresentationDefaults {
215215
static let fontSize: Double = 48.0
216216
static let minFontSize: Double = 12.0
217217
static let maxFontSize: Double = 200.0
@@ -225,7 +225,7 @@ enum PresentationDefaults {
225225
}
226226

227227
// MARK: - Bible Book Names (English canonical order)
228-
enum BibleBookNames {
228+
nonisolated enum BibleBookNames {
229229
static let oldTestament: [String] = [
230230
"Genesis", "Exodus", "Leviticus", "Numbers", "Deuteronomy",
231231
"Joshua", "Judges", "Ruth", "1 Samuel", "2 Samuel",
@@ -252,7 +252,7 @@ enum BibleBookNames {
252252
// MARK: - Bible Book Categories (for color-coding like BibleShow)
253253
import SwiftUI
254254

255-
enum BibleBookCategory: String, CaseIterable {
255+
nonisolated enum BibleBookCategory: String, CaseIterable {
256256
case law // Genesis–Deuteronomy (1–5)
257257
case history // Joshua–Esther (6–17)
258258
case wisdom // Job–Song of Solomon (18–22)
@@ -367,7 +367,7 @@ enum BibleBookCategory: String, CaseIterable {
367367
}
368368

369369
// MARK: - OSIS Book ID Mapping
370-
enum OSISBookIDs {
370+
nonisolated enum OSISBookIDs {
371371
static let mapping: [String: String] = [
372372
"Gen": "Genesis", "Exod": "Exodus", "Lev": "Leviticus",
373373
"Num": "Numbers", "Deut": "Deuteronomy", "Josh": "Joshua",

TopPresenter/Core/DataMigration.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import SwiftData
99

1010
// MARK: - Schema Version 1 (Initial)
1111
enum SchemaV1: VersionedSchema {
12-
static var versionIdentifier = Schema.Version(1, 0, 0)
12+
static let versionIdentifier = Schema.Version(1, 0, 0)
1313

1414
static var models: [any PersistentModel.Type] {
1515
[
@@ -35,7 +35,7 @@ enum SchemaV1: VersionedSchema {
3535
// defaults — so V1→V2 is a lightweight migration (no data loss, no custom code).
3636
// SongVerse is retained as the flattened presentation cache of a song's active version.
3737
enum SchemaV2: VersionedSchema {
38-
static var versionIdentifier = Schema.Version(2, 0, 0)
38+
static let versionIdentifier = Schema.Version(2, 0, 0)
3939

4040
static var models: [any PersistentModel.Type] {
4141
[

TopPresenter/Core/PresentationManager.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -932,7 +932,8 @@ final class PresentationManager {
932932
}
933933
}
934934

935-
deinit {
935+
// isolated deinit (SE-0371): runs on the main actor — may touch isolated state.
936+
isolated deinit {
936937
stopScreenMonitoring()
937938
}
938939

0 commit comments

Comments
 (0)