From c1c1860a256f192d93e1f98e00efa98d6ac8cb91 Mon Sep 17 00:00:00 2001 From: Dmitrii Borodko Date: Fri, 16 Oct 2026 17:47:53 +0200 Subject: [PATCH 1/2] Support Swift 6.1 across platforms --- .github/workflows/checks.yml | 145 +++++++++++------- AGENTS.md | 8 +- Package.swift | 13 +- README.md | 11 +- .../Enigmatic.docc/CodingStrategies.md | 27 +++- .../Enigmatic/Enigmatic.docc/Development.md | 42 +++-- Sources/Enigmatic/Enigmatic.docc/Enigmatic.md | 18 ++- .../Enigmatic/Enigmatic.docc/Serialization.md | 5 +- .../Codec/Strategies/Codec+UnixDate.swift | 4 +- .../Codec/Strategies/Codec+UnixIntDate.swift | 4 +- .../Codec+UnixIntMillisecondsDate.swift | 9 +- .../Strategies/Codec+UnixIntSecondsDate.swift | 9 +- .../Codec+UnixMillisecondsDate.swift | 9 +- .../Strategies/Codec+UnixSecondsDate.swift | 9 +- Tests/EnigmaticTests/CodableTests.swift | 4 +- .../DocumentationExamplesTests.swift | 12 ++ .../StrategyContractTests.swift | 7 +- 17 files changed, 220 insertions(+), 116 deletions(-) diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index 5720d35..7ba3efc 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -17,51 +17,118 @@ concurrency: jobs: tests: if: github.event_name != 'pull_request' || github.event.label.name == 'run-ci' - name: ${{ matrix.os }} / ${{ matrix.configuration }} - runs-on: ${{ matrix.os }} + name: Swift ${{ matrix.swift }} / ${{ matrix.platform }} / ${{ matrix.configuration }} + runs-on: ${{ matrix.runner }} container: ${{ matrix.container }} strategy: fail-fast: false matrix: - os: [macos-26, ubuntu-24.04] + swift: ['6.1', '6.2.3', '6.4.1'] + platform: [macos, linux, windows] configuration: [debug, release] include: - - os: macos-26 - swift-version: 6.2.4 - - os: ubuntu-24.04 + - platform: macos + runner: macos-26 + - platform: linux + runner: ubuntu-24.04 + - platform: windows + runner: windows-2022 + - swift: '6.1' + platform: macos + runner: macos-15 + xcode: '16.3' + - swift: '6.2.3' + platform: macos + xcode: '26.2' + - swift: '6.4.1' + platform: macos + xcode: '26.6' + - swift: '6.1' + platform: linux + container: swift:6.1.0 + - swift: '6.2.3' + platform: linux container: swift:6.2.3 - swift-version: 6.2.3 + - swift: '6.4.1' + platform: linux + container: swift:6.4.1 + env: + SWIFT_VERSION: ${{ matrix.swift }} + BUILD_CONFIGURATION: ${{ matrix.configuration }} steps: - uses: actions/checkout@v6 - - name: Select Xcode 26.3 + - name: Select Xcode ${{ matrix.xcode }} if: runner.os == 'macOS' - run: echo 'DEVELOPER_DIR=/Applications/Xcode_26.3.app/Contents/Developer' >> "$GITHUB_ENV" - - name: Verify Swift ${{ matrix.swift-version }} env: - EXPECTED_SWIFT_VERSION: ${{ matrix.swift-version }} + XCODE_VERSION: ${{ matrix.xcode }} + run: echo "DEVELOPER_DIR=/Applications/Xcode_${XCODE_VERSION}.app/Contents/Developer" >> "$GITHUB_ENV" + - name: Install Swift ${{ matrix.swift }} + if: runner.os == 'Windows' || (runner.os == 'macOS' && matrix.swift == '6.4.1') + uses: compnerd/gha-setup-swift@86d1ddeaf915c282137601aeb0654ea0e7b7f17e + with: + swift-version: swift-${{ matrix.swift }}-release + swift-build: ${{ matrix.swift }}-RELEASE + - name: Verify Swift ${{ matrix.swift }} + if: runner.os != 'Windows' run: | swift --version - swift --version | grep -F "Swift version $EXPECTED_SWIFT_VERSION " + if [ "$SWIFT_VERSION" = '6.1' ]; then + swift --version | grep -Eq 'Swift version 6\.1(\.0)?[[:space:]]' + else + swift --version | grep -F "Swift version $SWIFT_VERSION " + fi + - name: Verify Swift ${{ matrix.swift }} for Windows x64 + if: runner.os == 'Windows' + shell: pwsh + run: | + $version = (swift --version) -join "`n" + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + Write-Output $version + $expectedVersion = [regex]::Escape($env:SWIFT_VERSION) + if ($env:SWIFT_VERSION -eq '6.1') { $expectedVersion += '(\.0)?' } + if ($version -notmatch "Swift version $expectedVersion(\s|$)") { + throw "Expected Swift $env:SWIFT_VERSION" + } + if ($version -notmatch 'Target: x86_64-unknown-windows-msvc') { + throw 'Expected the Windows x64 toolchain' + } - name: Functional tests - run: swift test -c ${{ matrix.configuration }} --skip PerformanceTests + if: runner.os != 'Windows' + run: swift test -c "$BUILD_CONFIGURATION" --skip PerformanceTests + - name: Windows functional tests + if: runner.os == 'Windows' + shell: pwsh + run: | + $vswhere = Join-Path ${env:ProgramFiles(x86)} 'Microsoft Visual Studio\Installer\vswhere.exe' + $vsPath = & $vswhere -latest -products '*' -requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64 -property installationPath + if (-not $vsPath) { throw 'Visual Studio C++ tools not found' } + Import-Module (Join-Path $vsPath 'Common7\Tools\Microsoft.VisualStudio.DevShell.dll') + Enter-VsDevShell -VsInstallPath $vsPath -SkipAutomaticLocation -DevCmdArguments '-arch=x64 -host_arch=x64' + $swiftFlags = @() + if ($env:SWIFT_VERSION -eq '6.4.1') { + # Work around Swift 6.4.1's build-system and debug-type assertion issues. + $swiftFlags = @('--build-system', 'native', '-Xswiftc', '-Xfrontend', '-Xswiftc', '-disable-round-trip-debug-types') + } + swift test -c $env:BUILD_CONFIGURATION @swiftFlags --skip PerformanceTests + exit $LASTEXITCODE - name: Coverage gate tests - if: matrix.os == 'macos-26' && matrix.configuration == 'debug' + if: matrix.platform == 'macos' && matrix.swift == '6.2.3' && matrix.configuration == 'debug' run: python3 -m unittest discover -s Scripts/tests - name: Coverage - if: matrix.os == 'macos-26' && matrix.configuration == 'debug' + if: matrix.platform == 'macos' && matrix.swift == '6.2.3' && matrix.configuration == 'debug' run: | swift test --enable-code-coverage --skip PerformanceTests cp "$(swift test --show-codecov-path)" coverage.json python3 Scripts/check-coverage.py coverage.json --minimum 95 - name: Upload coverage - if: always() && matrix.os == 'macos-26' && matrix.configuration == 'debug' && hashFiles('coverage.json') != '' + if: always() && matrix.platform == 'macos' && matrix.swift == '6.2.3' && matrix.configuration == 'debug' && hashFiles('coverage.json') != '' uses: actions/upload-artifact@v6 with: name: coverage path: coverage.json if-no-files-found: error - name: Build documentation - if: matrix.os == 'macos-26' && matrix.configuration == 'debug' + if: matrix.platform == 'macos' && matrix.swift == '6.2.3' && matrix.configuration == 'debug' run: | swift package dump-symbol-graph --minimum-access-level public enigmatic_symbols="$(dirname "$(swift build --show-bin-path)")/symbolgraph" @@ -78,53 +145,13 @@ jobs: --hosting-base-path Enigmatic \ --warnings-as-errors - name: Pack documentation - if: matrix.os == 'macos-26' && matrix.configuration == 'debug' + if: matrix.platform == 'macos' && matrix.swift == '6.2.3' && matrix.configuration == 'debug' run: tar -czf Enigmatic.doccarchive.tar.gz -C .build Enigmatic.doccarchive - name: Upload documentation - if: matrix.os == 'macos-26' && matrix.configuration == 'debug' + if: matrix.platform == 'macos' && matrix.swift == '6.2.3' && matrix.configuration == 'debug' uses: actions/upload-artifact@v6 with: name: documentation path: Enigmatic.doccarchive.tar.gz compression-level: 0 if-no-files-found: error - - windows: - if: github.event_name != 'pull_request' || github.event.label.name == 'run-ci' - name: windows-2022 / ${{ matrix.configuration }} - runs-on: windows-2022 - strategy: - fail-fast: false - matrix: - configuration: [debug, release] - defaults: - run: - shell: pwsh - steps: - - uses: actions/checkout@v6 - - name: Install Swift 6.4.1 - uses: compnerd/gha-setup-swift@86d1ddeaf915c282137601aeb0654ea0e7b7f17e - with: - swift-version: swift-6.4.1-release - swift-build: 6.4.1-RELEASE - - name: Verify Swift 6.4.1 for Windows x64 - run: | - $version = (swift --version) -join "`n" - if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } - Write-Output $version - if ($version -notmatch 'Swift version 6\.4\.1(\s|$)') { - throw 'Expected Swift 6.4.1' - } - if ($version -notmatch 'Target: x86_64-unknown-windows-msvc') { - throw 'Expected the Windows x64 toolchain' - } - - name: Functional tests - run: | - $vswhere = Join-Path ${env:ProgramFiles(x86)} 'Microsoft Visual Studio\Installer\vswhere.exe' - $vsPath = & $vswhere -latest -products '*' -requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64 -property installationPath - if (-not $vsPath) { throw 'Visual Studio C++ tools not found' } - Import-Module (Join-Path $vsPath 'Common7\Tools\Microsoft.VisualStudio.DevShell.dll') - Enter-VsDevShell -VsInstallPath $vsPath -SkipAutomaticLocation -DevCmdArguments '-arch=x64 -host_arch=x64' - # Work around Swift 6.4.1's build-system and debug-type assertion issues. - swift test -c ${{ matrix.configuration }} --build-system native -Xswiftc -Xfrontend -Xswiftc -disable-round-trip-debug-types --skip PerformanceTests - exit $LASTEXITCODE diff --git a/AGENTS.md b/AGENTS.md index 5e87f39..22db1ee 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,9 +1,11 @@ # Project guidance Enigmatic is a dependency-free SwiftPM library for Codable value trees and -composable coding strategies. It requires Swift 6.2.3; Checks uses 6.2.4 on -macOS 26 and 6.2.3 on Ubuntu 24.04. Release uses 6.3.3 to build the previous -API baseline. +composable coding strategies. It requires Swift 6.1; generic Unix date strategies +require Swift 6.2.3. Checks separately tests the original Swift 6.1 release, +Swift 6.2.3, and Swift 6.4.1 on macOS, Linux, and Windows in Debug and Release. +Coverage and DocC use macOS with Swift 6.2.3. Release uses 6.3.3 to build the +previous API baseline. Follow the existing two-space Swift style. Preserve typed throws, runtime availability guards, and visibility required by `@inlinable` code. Update the diff --git a/Package.swift b/Package.swift index 018f69d..2bb970a 100644 --- a/Package.swift +++ b/Package.swift @@ -1,18 +1,25 @@ -// swift-tools-version:6.2.3 +// swift-tools-version:6.1 // The swift-tools-version declares the minimum version of Swift required to build this package. import PackageDescription +#if compiler(>=6.3) && compiler(<6.4) +let swiftSettings: [SwiftSetting] = [.enableExperimentalFeature("AnyAppleOSAvailability")] +#else +let swiftSettings: [SwiftSetting] = [] +#endif + let package = Package( name: "Enigmatic", products: [ .library(name: "Enigmatic", targets: ["Enigmatic"]), ], targets: [ - .target(name: "Enigmatic"), + .target(name: "Enigmatic", swiftSettings: swiftSettings), .testTarget( name: "EnigmaticTests", - dependencies: ["Enigmatic"] + dependencies: ["Enigmatic"], + swiftSettings: swiftSettings ), ] ) diff --git a/README.md b/README.md index 86f8523..a46ca5d 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ [![Checks](https://github.com/SwiftUtility/Enigmatic/actions/workflows/checks.yml/badge.svg?branch=main&event=push)](https://github.com/SwiftUtility/Enigmatic/actions/workflows/checks.yml) [![Release](https://img.shields.io/github/v/release/SwiftUtility/Enigmatic)](https://github.com/SwiftUtility/Enigmatic/releases/latest) -[![Swift](https://img.shields.io/badge/Swift-6.2.3%2B-orange?logo=swift)](Package.swift) +[![Swift](https://img.shields.io/badge/Swift-6.1%2B-orange?logo=swift)](Package.swift) [![SwiftPM](https://img.shields.io/badge/Swift_Package_Manager-compatible-brightgreen)](#install) [![License](https://img.shields.io/github/license/SwiftUtility/Enigmatic)](LICENSE) [![Coverage](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftutility.github.io%2FEnigmatic%2Fcoverage-badge.json)](https://github.com/SwiftUtility/Enigmatic/actions/workflows/release.yml) @@ -34,12 +34,14 @@ Foundation's `JSONDecoder`. With that same decoder, Enigma and PotentCodables' ## Install -Requires Swift 6.2.3 or newer. Add +Requires Swift 6.1 or newer. Add [SwiftUtility/Enigmatic](https://github.com/SwiftUtility/Enigmatic) in Xcode and link the **Enigmatic** product to your target. For SwiftPM targets, use `.product(name: "Enigmatic", package: "Enigmatic")`. -Some APIs require newer Apple OS versions; see the +The generic Unix date strategies require Swift 6.2.3 or newer; use the named +seconds/milliseconds strategies with older compilers. Some APIs also require +newer Apple OS versions; see the [compatibility notes](https://swiftutility.github.io/Enigmatic/documentation/enigmatic/#Compatibility). ## Edit a model @@ -107,7 +109,8 @@ models that encode as dictionaries with no overlapping keys. the usual date/data handling in `PropertyListEncoder`. Data becomes a byte array; Date becomes seconds since 2001-01-01. Use `@Codec.Box` for Base64 and `@Codec.Box` for seconds since 2001-01-01, or - `Codec.UnixDate<0>` for Unix seconds on supported OS versions. + `Codec.UnixSecondsDate` for Unix seconds. With Swift 6.2.3 or newer, prefer + `Codec.UnixDate<0>` on supported OS versions. - `Float` and `Double` can store `NaN` and positive or negative infinity in memory. These values are not valid JSON numbers. Default JSON encoding and `asJsonObject` reject them. When encoding or decoding the tree with your own diff --git a/Sources/Enigmatic/Enigmatic.docc/CodingStrategies.md b/Sources/Enigmatic/Enigmatic.docc/CodingStrategies.md index ac59b8a..a599bbd 100644 --- a/Sources/Enigmatic/Enigmatic.docc/CodingStrategies.md +++ b/Sources/Enigmatic/Enigmatic.docc/CodingStrategies.md @@ -70,9 +70,9 @@ when choosing a specific representation. > the exact behavior. Apply `Codec.Box` strategies before building the tree. Use `Base64Data` for Data -and `PlistDate` for seconds since 2001-01-01; `UnixDate<0>` is an alternative for -Unix seconds on supported runtimes. For Float and Double, define a string strategy -that preserves special values explicitly: +and `PlistDate` for seconds since 2001-01-01; `UnixSecondsDate` is an alternative +for Unix seconds on all supported compilers. For Float and Double, define a string +strategy that preserves special values explicitly: ```swift import Foundation @@ -173,8 +173,25 @@ timestamp need not survive a decode/encode round trip unchanged. For example, the stored interval is just below two milliseconds, and encoding truncates toward zero. The named integer strategies have the same precision limitation. -The generic Unix strategies require Apple OS 26; the named seconds and -milliseconds strategies remain available for older deployment targets. +The generic Unix strategies require Swift 6.2.3 or newer. On Apple platforms, +they also require OS 26. With earlier compilers or older deployment targets, use +`UnixSecondsDate` / `UnixIntSecondsDate` or +`UnixMillisecondsDate` / `UnixIntMillisecondsDate`. The named strategies are +deprecated on Apple OS 26 only when compiled with Swift 6.2.3 or newer; earlier +compilers retain them without deprecation warnings. Other scales require the +generic strategies or a custom strategy. + +For example, this model uses integer milliseconds on every supported compiler: + +```swift +struct Event: Codable { + @Codec.Box var timestamp: Date +} + +let event = Event(timestamp: Date(timeIntervalSince1970: -123.25)) +let tree = try Enigma(encode: event) +// {"timestamp": -123250} +``` ## Complexity diff --git a/Sources/Enigmatic/Enigmatic.docc/Development.md b/Sources/Enigmatic/Enigmatic.docc/Development.md index b8043e0..4097a81 100644 --- a/Sources/Enigmatic/Enigmatic.docc/Development.md +++ b/Sources/Enigmatic/Enigmatic.docc/Development.md @@ -12,13 +12,21 @@ the functional suite in the official Swift Docker image. The source mount is read-only; build artifacts stay in the container: ```sh -docker run --rm -v "$PWD:/package:ro" -w /package swift:6.2.3 \ +docker run --rm -v "$PWD:/package:ro" -w /package swift:6.1.0 \ swift test --scratch-path /tmp/enigmatic-build --skip PerformanceTests -docker run --rm -v "$PWD:/package:ro" -w /package swift:6.2.3 \ +docker run --rm -v "$PWD:/package:ro" -w /package swift:6.1.0 \ swift test --scratch-path /tmp/enigmatic-build -c release --skip PerformanceTests ``` -Apple-specific branches still require validation on macOS with Swift 6.2.3. +These commands pin the original Swift 6.1 release. The shorter `swift:6.1` tag +can resolve to a later 6.1 patch release. Local validation with Swift 6.1 on +macOS and Windows remains outstanding. +The generic Unix date strategies and their tests are compiled only with Swift +6.2.3 or newer; use the named seconds/milliseconds strategies on older compilers. + +The manifest enables `AnyAppleOSAvailability` only with Swift 6.3, where it is +experimental. Swift 6.4 supports `anyAppleOS` without the flag. Earlier compilers +use the explicit Apple platform fallbacks in conditional compilation guards. On Windows, use Developer PowerShell for Visual Studio with the C++ tools and Windows SDK installed. Start a fresh shell after installing Swift so `PATH` and @@ -45,22 +53,29 @@ These are toolchain workarounds, not package settings. The native build system i deprecated; recheck these workarounds when updating Swift. Windows validation does not replace the macOS and Linux CI jobs below. -The Checks workflow uses Swift 6.2.4 from Xcode 26.3 on macOS 26 and the -`swift:6.2.3` container on Ubuntu 24.04 to test the minimum supported compiler. -Benchmarks uses the same Xcode version as Checks. -Each job verifies the compiler version before testing. -The macOS Debug job also checks the coverage script, enforces 95% library line +The Checks workflow runs each compiler independently on all three platforms in +Debug and Release, producing 18 builds: + +| Swift | macOS | Linux (`ubuntu-24.04`) | Windows (`windows-2022`) | +| --- | --- | --- | --- | +| 6.1 | `macos-15`, Xcode 16.3 | `swift:6.1.0` | `swift-6.1-RELEASE` installer | +| 6.2.3 | `macos-26`, Xcode 26.2 | `swift:6.2.3` | `swift-6.2.3-RELEASE` installer | +| 6.4.1 | `macos-26`, official 6.4.1 toolchain with Xcode 26.6's SDK | `swift:6.4.1` | `swift-6.4.1-RELEASE` installer | + +Benchmarks uses Xcode 26.3. +Each job verifies its compiler version before testing. The 6.1 builds require +the original release (reported as 6.1 or 6.1.0), rejecting later patch versions. +The macOS 6.2.3 Debug job also checks the coverage script, enforces 95% library line coverage, and builds DocC. It uploads the coverage report as `coverage.json` and the documentation as `Enigmatic.doccarchive.tar.gz`, preserving DocC filenames inside the archive. -Checks also runs separate Windows x64 Debug and Release jobs on `windows-2022` -with Swift 6.4.1, Visual Studio's C++ environment, and the workarounds above. -These jobs do not change the macOS/Linux checks or the Release workflow. +All Windows toolchains use Visual Studio's C++ environment. Only Swift 6.4.1 +uses the build-system and debug-type workarounds above. Release validation uses Swift 6.3.3 from Xcode 26.6 and the `swift:6.3.3` container so the API gate can build previous releases requiring Swift 6.3. -The Linux Checks jobs validate compatibility with the minimum supported compiler. +The Swift 6.1 Checks jobs validate the minimum compiler independently. The Release workflow publishes the DocC site to GitHub Pages after all release validation jobs pass and before creating the GitHub release. The site is at @@ -99,7 +114,8 @@ be present on the default branch for their **Run workflow** buttons to appear. swift test -c release --filter PerformanceTests ``` -Build documentation with Xcode's documentation compiler. Set `enigmatic_symbols` +Build documentation with Swift 6.2.3 or newer and Xcode's documentation compiler +so links to the generic Unix date strategies resolve. Set `enigmatic_symbols` to the directory printed by `dump-symbol-graph`; the location depends on SwiftPM's build system. Copy only the library graphs, since SwiftPM can also emit a test-runner graph after tests have been built. diff --git a/Sources/Enigmatic/Enigmatic.docc/Enigmatic.md b/Sources/Enigmatic/Enigmatic.docc/Enigmatic.md index 257b461..0cd1585 100644 --- a/Sources/Enigmatic/Enigmatic.docc/Enigmatic.md +++ b/Sources/Enigmatic/Enigmatic.docc/Enigmatic.md @@ -9,8 +9,9 @@ dates and data. Encode a model into a tree, modify selected paths, merge a secon model, then decode the result. ``Codec`` provides reusable coding strategies that compose through a property wrapper. -The package requires Swift 6.2.3. Availability annotations on individual declarations -specify Apple runtime requirements; Linux and Windows use the toolchain runtime. +The package requires Swift 6.1. The generic Unix date strategies require Swift +6.2.3 or newer. Availability annotations on individual declarations specify Apple +runtime requirements; Linux and Windows use the toolchain runtime. ## Serialization limitations @@ -20,8 +21,9 @@ specify Apple runtime requirements; Linux and Windows use the toolchain runtime. > rejects them. Apply ``Codec/Box`` strategies before building the tree to choose > a representation independently of Foundation encoder/decoder settings. -Use ``Codec/Base64Data`` for Base64, ``Codec/PlistDate`` or ``Codec/UnixDate`` -for date units, and a custom string strategy for non-finite numbers. See +Use ``Codec/Base64Data`` for Base64, ``Codec/PlistDate`` or +``Codec/UnixSecondsDate`` for date units, and a custom string strategy for +non-finite numbers. See for the limits and for a complete example. ## Compatibility @@ -30,12 +32,14 @@ The package supports macOS, iOS, tvOS, watchOS, visionOS, Linux, and Windows. Other platforms produce a compile-time error instead of using a fallback implementation. The manifest leaves deployment targets at SwiftPM's defaults. Some APIs have -higher runtime requirements: +additional compiler or runtime requirements: - `Codec.Each`: macOS 14, iOS/tvOS 17, watchOS 10. Use Each2/Each3/Each4 on older systems. - Int128/UInt128 access: macOS 15, iOS/tvOS 18, watchOS 11, visionOS 2. -- `Codec.UnixDate` and `Codec.UnixIntDate`: Apple OS 26. - Use the named seconds/milliseconds strategies on older systems. +- `Codec.UnixDate` and `Codec.UnixIntDate`: Swift 6.2.3 or newer + and, on Apple platforms, OS 26. Use the named seconds/milliseconds strategies + with earlier compilers or older systems. Their Apple OS 26 deprecations apply + only with Swift 6.2.3 or newer. Linux and Windows use the toolchain runtime without Apple OS availability limits. diff --git a/Sources/Enigmatic/Enigmatic.docc/Serialization.md b/Sources/Enigmatic/Enigmatic.docc/Serialization.md index ebc9f1b..39c84bd 100644 --- a/Sources/Enigmatic/Enigmatic.docc/Serialization.md +++ b/Sources/Enigmatic/Enigmatic.docc/Serialization.md @@ -89,8 +89,9 @@ this JSON even with `.secondsSince1970` and `.base64` selected: ``` Use `@Codec.Box` for Base64 data and a date strategy such as -`Codec.PlistDate` (reference-date seconds) or `Codec.UnixDate<0>` (Unix seconds -on supported runtimes) before building the tree to choose a wire format. +`Codec.PlistDate` (reference-date seconds) or `Codec.UnixSecondsDate` (Unix +seconds) before building the tree to choose a wire format. With Swift 6.2.3 or +newer, prefer `Codec.UnixDate<0>` on supported runtimes. `Enigma.init(from:)` does not recognize date units or Base64 text on its own. Decoding a native `.date` or `.data` node as Date or Data returns the stored value; other representations follow Foundation's Decodable implementation. diff --git a/Sources/Enigmatic/Public/Codec/Strategies/Codec+UnixDate.swift b/Sources/Enigmatic/Public/Codec/Strategies/Codec+UnixDate.swift index ffb5e35..0c778b0 100644 --- a/Sources/Enigmatic/Public/Codec/Strategies/Codec+UnixDate.swift +++ b/Sources/Enigmatic/Public/Codec/Strategies/Codec+UnixDate.swift @@ -1,10 +1,11 @@ import Foundation +#if compiler(>=6.2.3) extension Codec { /// Encodes seconds since 1970 multiplied by 10 to the given scale as Double. /// /// Rejects non-finite values, invalid scale factors and unrepresentable results. -#if compiler(<6.4) +#if compiler(<6.3) @available(macOS 26, iOS 26, tvOS 26, watchOS 26, visionOS 26, *) #else @available(anyAppleOS 26, *) @@ -72,3 +73,4 @@ extension Codec { public typealias BoxedValue = Date } } +#endif diff --git a/Sources/Enigmatic/Public/Codec/Strategies/Codec+UnixIntDate.swift b/Sources/Enigmatic/Public/Codec/Strategies/Codec+UnixIntDate.swift index b4e7ea1..cae0fcc 100644 --- a/Sources/Enigmatic/Public/Codec/Strategies/Codec+UnixIntDate.swift +++ b/Sources/Enigmatic/Public/Codec/Strategies/Codec+UnixIntDate.swift @@ -1,10 +1,11 @@ import Foundation +#if compiler(>=6.2.3) extension Codec { /// Encodes seconds since 1970 multiplied by 10 to the given scale as Int, truncating toward zero. /// /// Rejects non-finite values, invalid scale factors and unrepresentable results. -#if compiler(<6.4) +#if compiler(<6.3) @available(macOS 26, iOS 26, tvOS 26, watchOS 26, visionOS 26, *) #else @available(anyAppleOS 26, *) @@ -72,3 +73,4 @@ extension Codec { public typealias BoxedValue = Date } } +#endif diff --git a/Sources/Enigmatic/Public/Codec/Strategies/Codec+UnixIntMillisecondsDate.swift b/Sources/Enigmatic/Public/Codec/Strategies/Codec+UnixIntMillisecondsDate.swift index d4bd718..f98b688 100644 --- a/Sources/Enigmatic/Public/Codec/Strategies/Codec+UnixIntMillisecondsDate.swift +++ b/Sources/Enigmatic/Public/Codec/Strategies/Codec+UnixIntMillisecondsDate.swift @@ -2,15 +2,16 @@ import Foundation extension Codec { /// Encodes milliseconds since 1970 as a truncated integer. - /// Prefer `UnixIntDate<3>` when the deployment target supports it. -#if compiler(<6.4) + /// Prefer `UnixIntDate<3>` when both the compiler (Swift 6.2.3 or newer) and + /// deployment target support it. +#if compiler(>=6.3) + @available(anyAppleOS, deprecated: 26.0) +#elseif compiler(>=6.2.3) @available(macOS, deprecated: 26.0) @available(iOS, deprecated: 26.0) @available(tvOS, deprecated: 26.0) @available(watchOS, deprecated: 26.0) @available(visionOS, deprecated: 26.0) -#else - @available(anyAppleOS, deprecated: 26.0) #endif public enum UnixIntMillisecondsDate: Hashable, DecodeStrategy, EncodeStrategy { /// Decodes integer milliseconds since 1970 into a date. diff --git a/Sources/Enigmatic/Public/Codec/Strategies/Codec+UnixIntSecondsDate.swift b/Sources/Enigmatic/Public/Codec/Strategies/Codec+UnixIntSecondsDate.swift index 3ac1093..fc41cdb 100644 --- a/Sources/Enigmatic/Public/Codec/Strategies/Codec+UnixIntSecondsDate.swift +++ b/Sources/Enigmatic/Public/Codec/Strategies/Codec+UnixIntSecondsDate.swift @@ -2,15 +2,16 @@ import Foundation extension Codec { /// Encodes seconds since 1970 as a truncated integer. - /// Prefer `UnixIntDate<0>` when the deployment target supports it. -#if compiler(<6.4) + /// Prefer `UnixIntDate<0>` when both the compiler (Swift 6.2.3 or newer) and + /// deployment target support it. +#if compiler(>=6.3) + @available(anyAppleOS, deprecated: 26.0) +#elseif compiler(>=6.2.3) @available(macOS, deprecated: 26.0) @available(iOS, deprecated: 26.0) @available(tvOS, deprecated: 26.0) @available(watchOS, deprecated: 26.0) @available(visionOS, deprecated: 26.0) -#else - @available(anyAppleOS, deprecated: 26.0) #endif public enum UnixIntSecondsDate: Hashable, DecodeStrategy, EncodeStrategy, Error { /// Decodes integer seconds since 1970 into a date. diff --git a/Sources/Enigmatic/Public/Codec/Strategies/Codec+UnixMillisecondsDate.swift b/Sources/Enigmatic/Public/Codec/Strategies/Codec+UnixMillisecondsDate.swift index ffae04e..fa670f2 100644 --- a/Sources/Enigmatic/Public/Codec/Strategies/Codec+UnixMillisecondsDate.swift +++ b/Sources/Enigmatic/Public/Codec/Strategies/Codec+UnixMillisecondsDate.swift @@ -2,15 +2,16 @@ import Foundation extension Codec { /// Encodes seconds since 1970 multiplied by 1,000 as a floating-point value. - /// Prefer `UnixDate<3>` when the deployment target supports it. -#if compiler(<6.4) + /// Prefer `UnixDate<3>` when both the compiler (Swift 6.2.3 or newer) and + /// deployment target support it. +#if compiler(>=6.3) + @available(anyAppleOS, deprecated: 26.0) +#elseif compiler(>=6.2.3) @available(macOS, deprecated: 26.0) @available(iOS, deprecated: 26.0) @available(tvOS, deprecated: 26.0) @available(watchOS, deprecated: 26.0) @available(visionOS, deprecated: 26.0) -#else - @available(anyAppleOS, deprecated: 26.0) #endif public enum UnixMillisecondsDate: Hashable, DecodeStrategy, EncodeStrategy, Error { /// Decodes finite milliseconds since 1970 into a date. diff --git a/Sources/Enigmatic/Public/Codec/Strategies/Codec+UnixSecondsDate.swift b/Sources/Enigmatic/Public/Codec/Strategies/Codec+UnixSecondsDate.swift index 454c0a6..d002ed0 100644 --- a/Sources/Enigmatic/Public/Codec/Strategies/Codec+UnixSecondsDate.swift +++ b/Sources/Enigmatic/Public/Codec/Strategies/Codec+UnixSecondsDate.swift @@ -2,15 +2,16 @@ import Foundation extension Codec { /// Encodes seconds since 1970 as a floating-point value. - /// Prefer `UnixDate<0>` when the deployment target supports it. -#if compiler(<6.4) + /// Prefer `UnixDate<0>` when both the compiler (Swift 6.2.3 or newer) and + /// deployment target support it. +#if compiler(>=6.3) + @available(anyAppleOS, deprecated: 26.0) +#elseif compiler(>=6.2.3) @available(macOS, deprecated: 26.0) @available(iOS, deprecated: 26.0) @available(tvOS, deprecated: 26.0) @available(watchOS, deprecated: 26.0) @available(visionOS, deprecated: 26.0) -#else - @available(anyAppleOS, deprecated: 26.0) #endif public enum UnixSecondsDate: Hashable, DecodeStrategy, EncodeStrategy, Error { /// Decodes finite seconds since 1970 into a date. diff --git a/Tests/EnigmaticTests/CodableTests.swift b/Tests/EnigmaticTests/CodableTests.swift index 89787ea..1113f9f 100644 --- a/Tests/EnigmaticTests/CodableTests.swift +++ b/Tests/EnigmaticTests/CodableTests.swift @@ -176,7 +176,8 @@ final class CodableTests: XCTestCase { XCTAssertEqual(direct, indirect) } -#if compiler(<6.4) +#if compiler(>=6.2.3) +#if compiler(<6.3) @available(macOS 26, iOS 26, tvOS 26, watchOS 26, visionOS 26, *) #else @available(anyAppleOS 26, *) @@ -200,4 +201,5 @@ final class CodableTests: XCTestCase { ) Checker().check(value: codecDates) } +#endif } diff --git a/Tests/EnigmaticTests/DocumentationExamplesTests.swift b/Tests/EnigmaticTests/DocumentationExamplesTests.swift index a022b61..2ee66c2 100644 --- a/Tests/EnigmaticTests/DocumentationExamplesTests.swift +++ b/Tests/EnigmaticTests/DocumentationExamplesTests.swift @@ -38,6 +38,18 @@ final class DocumentationExamplesTests: XCTestCase { XCTAssertNil(try Enigma.dictionary(["bytes": "AQID", "link": nil]).decode(Payload.self).link) } + func testDocCNamedUnixDateExample() throws { + struct Event: Codable { + @Codec.Box var timestamp: Date + } + let event = Event(timestamp: Date(timeIntervalSince1970: -123.25)) + let tree = try Enigma(encode: event) + XCTAssertEqual(tree, ["timestamp": -123250]) + XCTAssertEqual(try tree.decode(Event.self).timestamp, event.timestamp) + let json = try JSONEncoder().encode(event) + XCTAssertEqual(try JSONDecoder().decode(Event.self, from: json).timestamp, event.timestamp) + } + func testDocCTreeExample() { var tree: Enigma = ["items": [[:], ["name": "first"]]] tree["items", 1, "name"] = "second" diff --git a/Tests/EnigmaticTests/StrategyContractTests.swift b/Tests/EnigmaticTests/StrategyContractTests.swift index a4fd833..9f14863 100644 --- a/Tests/EnigmaticTests/StrategyContractTests.swift +++ b/Tests/EnigmaticTests/StrategyContractTests.swift @@ -741,7 +741,8 @@ final class StrategyContractTests: XCTestCase { } } -#if compiler(<6.4) +#if compiler(>=6.2.3) +#if compiler(<6.3) @available(macOS 26, iOS 26, tvOS 26, watchOS 26, visionOS 26, *) #else @available(anyAppleOS 26, *) @@ -753,6 +754,9 @@ final class StrategyContractTests: XCTestCase { try roundTrip(Codec.UnixDate<6>.self, date, expected: -123250000) try roundTrip(Codec.UnixIntDate<3>.self, date, expected: -123250) try roundTrip(Codec.UnixIntDate<6>.self, date, expected: -123250000) + let negativeScaleDate = Date(timeIntervalSince1970: -125000) + try roundTrip(Codec.UnixDate< -3 >.self, negativeScaleDate, expected: .double(-125)) + try roundTrip(Codec.UnixIntDate< -3 >.self, negativeScaleDate, expected: -125) let truncated = try Enigma(encode: Codec.Box>(wrappedValue: date)) XCTAssertEqual(truncated, -123) XCTAssertEqual(try truncated.decode(Codec.Box>.self).wrappedValue, Date(timeIntervalSince1970: -123)) @@ -770,6 +774,7 @@ final class StrategyContractTests: XCTestCase { XCTAssertThrowsError(try Enigma.double(.greatestFiniteMagnitude).decode(Codec.Box>.self)) XCTAssertThrowsError(try Enigma.int(Int.max).decode(Codec.Box>.self)) } +#endif private func checkInvalidScale(_ strategy: S.Type) throws where S.BoxedValue == Date { assertDecodingError("dataCorrupted", path: []) { _ = try Enigma.int(1).decode(Codec.Box.self) } From dde64744b33cd691889e121032943b2e74920e3a Mon Sep 17 00:00:00 2001 From: Dmitrii Borodko Date: Fri, 16 Oct 2026 18:55:41 +0200 Subject: [PATCH 2/2] Fix Swift 6.1 and cross-platform CI failures --- .github/workflows/checks.yml | 17 ++++++++++--- .../Enigmatic.docc/CodingStrategies.md | 2 +- .../Enigmatic/Enigmatic.docc/Development.md | 20 ++++++++++----- .../Enigmatic/Public/Codec/Codec+Each.swift | 25 +++++++++++++++++++ .../Codec/Strategies/Codec+PathURL.swift | 1 + .../StrategyContractTests.swift | 2 ++ 6 files changed, 57 insertions(+), 10 deletions(-) diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index 7ba3efc..1701a48 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -94,7 +94,14 @@ jobs: } - name: Functional tests if: runner.os != 'Windows' - run: swift test -c "$BUILD_CONFIGURATION" --skip PerformanceTests + shell: bash + run: | + swiftFlags=() + if [ "$RUNNER_OS" = 'Linux' ] && [ "$SWIFT_VERSION" = '6.4.1' ]; then + # Avoid Swift 6.4.1's Swift Build planning crash on Linux. + swiftFlags+=(--build-system native) + fi + swift test -c "$BUILD_CONFIGURATION" "${swiftFlags[@]}" --skip PerformanceTests - name: Windows functional tests if: runner.os == 'Windows' shell: pwsh @@ -105,9 +112,13 @@ jobs: Import-Module (Join-Path $vsPath 'Common7\Tools\Microsoft.VisualStudio.DevShell.dll') Enter-VsDevShell -VsInstallPath $vsPath -SkipAutomaticLocation -DevCmdArguments '-arch=x64 -host_arch=x64' $swiftFlags = @() + if ($env:SWIFT_VERSION -eq '6.2.3' -or $env:SWIFT_VERSION -eq '6.4.1') { + # Negative integer generic arguments trip the compiler's debug-type check. + $swiftFlags += @('-Xswiftc', '-Xfrontend', '-Xswiftc', '-disable-round-trip-debug-types') + } if ($env:SWIFT_VERSION -eq '6.4.1') { - # Work around Swift 6.4.1's build-system and debug-type assertion issues. - $swiftFlags = @('--build-system', 'native', '-Xswiftc', '-Xfrontend', '-Xswiftc', '-disable-round-trip-debug-types') + # Work around Swift 6.4.1's build-system header lookup issue. + $swiftFlags += @('--build-system', 'native') } swift test -c $env:BUILD_CONFIGURATION @swiftFlags --skip PerformanceTests exit $LASTEXITCODE diff --git a/Sources/Enigmatic/Enigmatic.docc/CodingStrategies.md b/Sources/Enigmatic/Enigmatic.docc/CodingStrategies.md index a599bbd..b55c5ad 100644 --- a/Sources/Enigmatic/Enigmatic.docc/CodingStrategies.md +++ b/Sources/Enigmatic/Enigmatic.docc/CodingStrategies.md @@ -21,7 +21,7 @@ the present payload of an optional strategy. `Base64Data` encodes a Base64 string and rejects malformed input. `StringURL` uses Foundation's URL initializer, which permits relative URLs; it does not enforce an HTTP scheme. `PathURL` turns filesystem paths into file URLs and encodes their -unescaped paths. Relative paths resolve against the process's working directory; +unescaped absolute paths. Relative paths resolve against the process's working directory; the original relative spelling and URL base are not preserved. Encoding a non-file URL still writes only its path. On Windows, `PathURL` accepts drive and network paths with either slash style. diff --git a/Sources/Enigmatic/Enigmatic.docc/Development.md b/Sources/Enigmatic/Enigmatic.docc/Development.md index 4097a81..0bea13a 100644 --- a/Sources/Enigmatic/Enigmatic.docc/Development.md +++ b/Sources/Enigmatic/Enigmatic.docc/Development.md @@ -35,10 +35,10 @@ Windows SDK installed. Start a fresh shell after installing Swift so `PATH` and The functional suite passed locally in Debug and Release on Windows x64 with Swift 6.4.1 on 2026-10-16. -Local Windows x64 validation with Swift 6.4.1 encountered two toolchain issues: -the default build system failed to find `stdlib.h`, and the compiler's debug-type -round-trip check asserted on negative integer generic arguments in the strategy -tests. To reproduce the successful validation, use: +Windows CI with Swift 6.2.3 and 6.4.1 needs the compiler's debug-type round-trip +check disabled for negative integer generic arguments in the strategy tests. +Swift 6.4.1 also needs the native build system because the default build system +failed to find `stdlib.h`. To reproduce the successful local 6.4.1 validation, use: ```powershell $windowsSwiftFlags = @( @@ -53,6 +53,11 @@ These are toolchain workarounds, not package settings. The native build system i deprecated; recheck these workarounds when updating Swift. Windows validation does not replace the macOS and Linux CI jobs below. +The Windows Swift 6.1 compiler also asserts while generating `Each`'s encoding +witness for a tuple pack ([Swift issue #79870](https://github.com/swiftlang/swift/issues/79870)). +A helper keeps the tuple inside `Codec.Box` across that +witness boundary on Windows with Swift 6.1. Other compilers use the direct path. + The Checks workflow runs each compiler independently on all three platforms in Debug and Release, producing 18 builds: @@ -70,8 +75,11 @@ coverage, and builds DocC. It uploads the coverage report as `coverage.json` and the documentation as `Enigmatic.doccarchive.tar.gz`, preserving DocC filenames inside the archive. -All Windows toolchains use Visual Studio's C++ environment. Only Swift 6.4.1 -uses the build-system and debug-type workarounds above. +All Windows toolchains use Visual Studio's C++ environment. Swift 6.2.3 and +6.4.1 use the debug-type workaround above; only 6.4.1 uses the native build system. +The Linux 6.4.1 jobs also select the native build system to avoid a crash during +Swift Build planning on the hosted x86_64 runner. These flags do not change the +compiler versions or skip any functional tests. Release validation uses Swift 6.3.3 from Xcode 26.6 and the `swift:6.3.3` container so the API gate can build previous releases requiring Swift 6.3. diff --git a/Sources/Enigmatic/Public/Codec/Codec+Each.swift b/Sources/Enigmatic/Public/Codec/Codec+Each.swift index ee807d1..535799e 100644 --- a/Sources/Enigmatic/Public/Codec/Codec+Each.swift +++ b/Sources/Enigmatic/Public/Codec/Codec+Each.swift @@ -89,6 +89,30 @@ extension Codec.Each: Codec.DecodeStrategy where repeat each Value: Codec.Decode } } +#if os(Windows) && compiler(<6.2) +// Swift 6.1's asserting compiler crashes when a witness unpacks a tuple pack. +// Keep the tuple inside Box across the witness boundary instead. +@usableFromInline +protocol EachEncodeStrategy: Codec.EncodeStrategy { + static func encodeBox(_ box: Codec.Box, encoder: some Encoder) throws +} + +extension EachEncodeStrategy { + @inlinable + public static func encode(value: BoxedValue, encoder: some Encoder) throws { + try encodeBox(Codec.Box(wrappedValue: value), encoder: encoder) + } +} + +extension Codec.Each: Codec.EncodeStrategy, EachEncodeStrategy where repeat each Value: Codec.EncodeStrategy { + @usableFromInline + static func encodeBox(_ box: Codec.Box, encoder: some Encoder) throws { + for (value, type) in repeat (each box.wrappedValue, (each Value).self) { + try type.encode(value: value, encoder: encoder) + } + } +} +#else @available(macOS 14.0, iOS 17.0, tvOS 17.0, watchOS 10.0, *) extension Codec.Each: Codec.EncodeStrategy where repeat each Value: Codec.EncodeStrategy { /// Encodes each component with its corresponding strategy into one encoder. @@ -100,3 +124,4 @@ extension Codec.Each: Codec.EncodeStrategy where repeat each Value: Codec.Encode } } } +#endif diff --git a/Sources/Enigmatic/Public/Codec/Strategies/Codec+PathURL.swift b/Sources/Enigmatic/Public/Codec/Strategies/Codec+PathURL.swift index 0baf94e..08d64c1 100644 --- a/Sources/Enigmatic/Public/Codec/Strategies/Codec+PathURL.swift +++ b/Sources/Enigmatic/Public/Codec/Strategies/Codec+PathURL.swift @@ -37,6 +37,7 @@ extension Codec { /// - Complexity: O(n) in the URL or path string length. @inlinable public static func encode(value: BoxedValue, encoder: some Encoder) throws { + let value = value.absoluteURL #if os(anyAppleOS) || os(macOS) || os(iOS) || os(tvOS) || os(watchOS) || os(visionOS) if #available(macOS 13.0, iOS 16.0, tvOS 16.0, watchOS 9.0, *) { try value.path(percentEncoded: false).encode(to: encoder) diff --git a/Tests/EnigmaticTests/StrategyContractTests.swift b/Tests/EnigmaticTests/StrategyContractTests.swift index 9f14863..d5f99e1 100644 --- a/Tests/EnigmaticTests/StrategyContractTests.swift +++ b/Tests/EnigmaticTests/StrategyContractTests.swift @@ -275,6 +275,8 @@ final class StrategyContractTests: XCTestCase { let tree = try Enigma(encode: box) XCTAssertEqual(tree, .string(expected.path)) XCTAssertEqual(try tree.decode(Codec.Box.self).wrappedValue.absoluteURL, expected) + let relative = URL(string: "file.txt", relativeTo: expected.deletingLastPathComponent())! + XCTAssertEqual(try Enigma(encode: Codec.Box(wrappedValue: relative)), tree) } func testPathURLEncodesOnlyTheUnescapedPathOfANonFileURL() throws {