diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0224e81..db50694 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,17 +14,42 @@ concurrency: jobs: swift: - name: Swift build + tests (macOS) - runs-on: macos-15 + name: Swift build + tests (${{ matrix.name }}) + runs-on: ${{ matrix.runner }} timeout-minutes: 45 + strategy: + fail-fast: false + matrix: + include: + - name: macOS 15, Xcode 26.2 + slug: macos15 + runner: macos-15 + xcode: "26.2" + # The OS most users are on: build with its SDK and run the tests on + # it. GitHub names this image after its Xcode; it runs macOS 27. + - name: macOS 27, Xcode 27 + slug: macos27 + runner: xcode-27 + xcode: "27" steps: - name: Checkout uses: actions/checkout@v4 - - name: Select Xcode + - name: Select Xcode ${{ matrix.xcode }} run: | - sudo xcode-select -s /Applications/Xcode_26.2.app 2>/dev/null \ - || sudo xcode-select -s "$(ls -d /Applications/Xcode_2*.app | sort -V | tail -1)" + sw_vers + # The newest release of this Xcode, not a beta. A version-named path + # can be a link to a beta (Xcode_27.2.app -> Xcode_27.2_beta.app), + # so judge each by where it really points. + XCODE="$(for app in /Applications/Xcode_${{ matrix.xcode }}*.app; do realpath "$app"; done \ + | grep -viE 'beta|release_candidate|_rc' | sort -uV | tail -1)" + if [ -z "$XCODE" ]; then + echo "::error::no release of Xcode ${{ matrix.xcode }} on this runner" + ls -l /Applications | grep Xcode + exit 1 + fi + echo "Using $XCODE" + sudo xcode-select -s "$XCODE" xcodebuild -version - name: Install XcodeGen @@ -37,8 +62,8 @@ jobs: uses: actions/cache@v4 with: path: build/SourcePackages - key: spm-${{ runner.os }}-${{ hashFiles('project.yml') }} - restore-keys: spm-${{ runner.os }}- + key: spm-${{ matrix.slug }}-${{ hashFiles('project.yml') }} + restore-keys: spm-${{ matrix.slug }}- - name: Resolve Swift package dependencies run: | @@ -122,7 +147,7 @@ jobs: if: always() uses: actions/upload-artifact@v4 with: - name: xcodebuild-logs + name: xcodebuild-logs-${{ matrix.slug }} path: | build/xcodebuild-build.log build/xcodebuild-test.log @@ -133,7 +158,7 @@ jobs: if: failure() uses: actions/upload-artifact@v4 with: - name: test-results + name: test-results-${{ matrix.slug }} path: build/TestResults.xcresult if-no-files-found: ignore diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 53ce1c4..2aa5f78 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -43,23 +43,44 @@ jobs: # One DMG per architecture rather than a universal app: each download is # half the size, and the Intel build says plainly that it cannot run the # local MLX engines. + # + # Apple silicon is built with Xcode 27 on macOS 27, so the app is linked + # against the macOS 27 SDK and gets that system's behavior rather than a + # compatibility mode. macOS 27 does not run on Intel Macs, so the Intel + # build stays on the toolchain of the last system they run. build: name: Build DMG (${{ matrix.arch }}) needs: [version, test] - runs-on: macos-15 + runs-on: ${{ matrix.runner }} strategy: matrix: - arch: [arm64, x86_64] + include: + - arch: arm64 + runner: xcode-27 + xcode: "27" + - arch: x86_64 + runner: macos-15 + xcode: "26.2" env: VERSION: ${{ needs.version.outputs.version }} steps: - name: Checkout uses: actions/checkout@v4 - - name: Select Xcode + - name: Select Xcode ${{ matrix.xcode }} run: | - sudo xcode-select -s /Applications/Xcode_26.2.app 2>/dev/null \ - || sudo xcode-select -s "$(ls -d /Applications/Xcode_2*.app | sort -V | tail -1)" + # The newest release of this Xcode, not a beta. A version-named path + # can be a link to a beta (Xcode_27.2.app -> Xcode_27.2_beta.app), + # so judge each by where it really points. + XCODE="$(for app in /Applications/Xcode_${{ matrix.xcode }}*.app; do realpath "$app"; done \ + | grep -viE 'beta|release_candidate|_rc' | sort -uV | tail -1)" + if [ -z "$XCODE" ]; then + echo "::error::no release of Xcode ${{ matrix.xcode }} on this runner" + ls -l /Applications | grep Xcode + exit 1 + fi + echo "Using $XCODE" + sudo xcode-select -s "$XCODE" xcodebuild -version - name: Install XcodeGen diff --git a/Core/Pipeline/TranscriptBuffer.swift b/Core/Pipeline/TranscriptBuffer.swift index 4c35797..77f387e 100644 --- a/Core/Pipeline/TranscriptBuffer.swift +++ b/Core/Pipeline/TranscriptBuffer.swift @@ -94,7 +94,7 @@ final class TranscriptBuffer: ObservableObject { /// the only signal, so a line whose translation failed, came back empty, was /// covered by a later T3PO translation or was never sent (translation off) /// showed as translating forever. -enum LiveTranslationState: Hashable { +enum LiveTranslationState: Hashable, Sendable { /// Not sent: translation was off, or the line's sentence has not ended. case notSent case pending @@ -105,7 +105,7 @@ enum LiveTranslationState: Hashable { case merged } -struct LiveSegment: Identifiable, Hashable { +struct LiveSegment: Identifiable, Hashable, Sendable { let id: Int64 // = rowId let rowId: Int64 let startMs: Int64 diff --git a/Features/Shared/SessionPresentation.swift b/Features/Shared/SessionPresentation.swift index 6e8d220..698dc0a 100644 --- a/Features/Shared/SessionPresentation.swift +++ b/Features/Shared/SessionPresentation.swift @@ -102,6 +102,9 @@ struct SentenceBlock: Identifiable { var id: Line.ID { lines[0].id } } +/// So the rows' nonisolated `==` can read a block off a main-actor view. +extension SentenceBlock: Sendable where Line: Sendable {} + extension Array where Element == Segment { var sentenceBlocks: [SentenceBlock] { SentenceGroups.group(self).map { SentenceBlock(lines: $0) } diff --git a/project.yml b/project.yml index 49d92d5..2290365 100644 --- a/project.yml +++ b/project.yml @@ -15,8 +15,8 @@ settings: SWIFT_STRICT_CONCURRENCY: complete SWIFT_TREAT_WARNINGS_AS_ERRORS: "NO" ENABLE_USER_SCRIPT_SANDBOXING: NO - MARKETING_VERSION: "0.16.6" - CURRENT_PROJECT_VERSION: "24" + MARKETING_VERSION: "0.16.7" + CURRENT_PROJECT_VERSION: "25" DEAD_CODE_STRIPPING: YES packages: