diff --git a/.github/workflows/benchmarks.yml b/.github/workflows/benchmarks.yml index 60aeb11..6f35395 100644 --- a/.github/workflows/benchmarks.yml +++ b/.github/workflows/benchmarks.yml @@ -14,13 +14,13 @@ jobs: benchmarks: runs-on: macos-26 env: - DEVELOPER_DIR: /Applications/Xcode_26.6.app/Contents/Developer + DEVELOPER_DIR: /Applications/Xcode_26.3.app/Contents/Developer steps: - uses: actions/checkout@v6 - - name: Verify Swift 6.3.3 + - name: Verify Swift 6.2.4 run: | swift --version - swift --version | grep -Eq 'Swift version 6\.3\.3([[:space:]]|$)' + swift --version | grep -Eq 'Swift version 6\.2\.4([[:space:]]|$)' - name: Release benchmarks without instrumentation shell: bash run: | diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index a7b9e86..5720d35 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -26,17 +26,22 @@ jobs: os: [macos-26, ubuntu-24.04] configuration: [debug, release] include: + - os: macos-26 + swift-version: 6.2.4 - os: ubuntu-24.04 - container: swift:6.3.3 + container: swift:6.2.3 + swift-version: 6.2.3 steps: - uses: actions/checkout@v6 - - name: Select Xcode 26.6 + - name: Select Xcode 26.3 if: runner.os == 'macOS' - run: echo 'DEVELOPER_DIR=/Applications/Xcode_26.6.app/Contents/Developer' >> "$GITHUB_ENV" - - name: Verify Swift 6.3.3 + 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 }} run: | swift --version - swift --version | grep -Eq 'Swift version 6\.3\.3([[:space:]]|$)' + swift --version | grep -F "Swift version $EXPECTED_SWIFT_VERSION " - name: Functional tests run: swift test -c ${{ matrix.configuration }} --skip PerformanceTests - name: Coverage gate tests diff --git a/AGENTS.md b/AGENTS.md index 77b9916..5e87f39 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,8 +1,9 @@ # Project guidance Enigmatic is a dependency-free SwiftPM library for Codable value trees and -composable coding strategies. It requires Swift 6.3; CI uses 6.3.3 on macOS 26 -and Ubuntu 24.04. +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. 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 c994e44..018f69d 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version:6.3 +// swift-tools-version:6.2.3 // The swift-tools-version declares the minimum version of Swift required to build this package. import PackageDescription @@ -9,25 +9,10 @@ let package = Package( .library(name: "Enigmatic", targets: ["Enigmatic"]), ], targets: [ - .target( - name: "Enigmatic", - swiftSettings: SwiftSetting.allSettings - ), + .target(name: "Enigmatic"), .testTarget( name: "EnigmaticTests", - dependencies: ["Enigmatic"], - swiftSettings: SwiftSetting.allSettings + dependencies: ["Enigmatic"] ), ] ) - -extension SwiftSetting { -#if compiler(<6.4) - static let anyAppleOSAvailability: [Self] = [.enableExperimentalFeature("AnyAppleOSAvailability")] -#else - static let anyAppleOSAvailability: [Self] = [] -#endif - static let allSettings: [Self] = [ - anyAppleOSAvailability, - ].flatMap(\.self) -} diff --git a/README.md b/README.md index bf39472..86f8523 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.3%2B-orange?logo=swift)](Package.swift) +[![Swift](https://img.shields.io/badge/Swift-6.2.3%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,7 +34,7 @@ Foundation's `JSONDecoder`. With that same decoder, Enigma and PotentCodables' ## Install -Requires Swift 6.3 or newer. Add +Requires Swift 6.2.3 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")`. diff --git a/Sources/Enigmatic/Enigmatic.docc/Development.md b/Sources/Enigmatic/Enigmatic.docc/Development.md index 7b3c70f..b8043e0 100644 --- a/Sources/Enigmatic/Enigmatic.docc/Development.md +++ b/Sources/Enigmatic/Enigmatic.docc/Development.md @@ -7,6 +7,19 @@ swift test --skip PerformanceTests swift test -c release --skip PerformanceTests ``` +To check Linux compatibility with the minimum compiler version from macOS, run +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 \ + swift test --scratch-path /tmp/enigmatic-build --skip PerformanceTests +docker run --rm -v "$PWD:/package:ro" -w /package swift:6.2.3 \ + swift test --scratch-path /tmp/enigmatic-build -c release --skip PerformanceTests +``` + +Apple-specific branches still require validation on macOS with Swift 6.2.3. + 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 `SDKROOT` include the toolchain and its SDK. @@ -32,9 +45,10 @@ 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 macOS and Linux CI jobs use Swift 6.3.3 from Xcode 26.6 on macOS 26 and the -`swift:6.3.3` container on Ubuntu 24.04. Each job verifies the compiler version -before testing. +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 coverage, and builds DocC. It uploads the coverage report as `coverage.json` and the documentation as `Enigmatic.doccarchive.tar.gz`, preserving DocC filenames @@ -44,6 +58,10 @@ 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. +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 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 . Enable diff --git a/Sources/Enigmatic/Enigmatic.docc/Enigmatic.md b/Sources/Enigmatic/Enigmatic.docc/Enigmatic.md index fd69413..257b461 100644 --- a/Sources/Enigmatic/Enigmatic.docc/Enigmatic.md +++ b/Sources/Enigmatic/Enigmatic.docc/Enigmatic.md @@ -9,8 +9,8 @@ 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.3. Availability annotations on individual declarations -specify Apple runtime requirements; Linux uses the toolchain runtime. +The package requires Swift 6.2.3. Availability annotations on individual declarations +specify Apple runtime requirements; Linux and Windows use the toolchain runtime. ## Serialization limitations @@ -26,6 +26,9 @@ for date units, and a custom string strategy for non-finite numbers. See ## Compatibility +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: @@ -34,7 +37,7 @@ higher runtime requirements: - `Codec.UnixDate` and `Codec.UnixIntDate`: Apple OS 26. Use the named seconds/milliseconds strategies on older systems. -Linux uses the toolchain runtime without Apple OS availability limits. +Linux and Windows use the toolchain runtime without Apple OS availability limits. ## Topics diff --git a/Sources/Enigmatic/Internal/Primitives/EnigmaConvertibleObject.swift b/Sources/Enigmatic/Internal/Primitives/EnigmaConvertibleObject.swift index 977efdf..eeef83a 100644 --- a/Sources/Enigmatic/Internal/Primitives/EnigmaConvertibleObject.swift +++ b/Sources/Enigmatic/Internal/Primitives/EnigmaConvertibleObject.swift @@ -45,8 +45,10 @@ extension NSValue: EnigmaConvertibleObject { func extract(seed: consuming T) -> T { #if os(anyAppleOS) || os(macOS) || os(iOS) || os(tvOS) || os(watchOS) || os(visionOS) withUnsafeMutablePointer(to: &seed) { getValue($0, size: MemoryLayout.size) } -#else +#elseif os(Linux) || os(Windows) withUnsafeMutablePointer(to: &seed) { getValue($0) } +#else +#error("Unsupported platform: Enigmatic supports Apple platforms, Linux, and Windows.") #endif return consume seed } diff --git a/Sources/Enigmatic/Public/Codec/Strategies/Codec+PathURL.swift b/Sources/Enigmatic/Public/Codec/Strategies/Codec+PathURL.swift index e9cd90d..0baf94e 100644 --- a/Sources/Enigmatic/Public/Codec/Strategies/Codec+PathURL.swift +++ b/Sources/Enigmatic/Public/Codec/Strategies/Codec+PathURL.swift @@ -23,10 +23,12 @@ extension Codec { } else { return URL(fileURLWithPath: value, isDirectory: value.hasSuffix("/")) } +#elseif os(Linux) + return URL(fileURLWithPath: value, isDirectory: value.hasSuffix("/")) #elseif os(Windows) return URL(fileURLWithPath: value, isDirectory: value.hasSuffix("/") || value.hasSuffix("\\")) #else - return URL(fileURLWithPath: value, isDirectory: value.hasSuffix("/")) +#error("Unsupported platform: Enigmatic supports Apple platforms, Linux, and Windows.") #endif } @@ -35,16 +37,20 @@ extension Codec { /// - Complexity: O(n) in the URL or path string length. @inlinable public static func encode(value: BoxedValue, encoder: some Encoder) throws { -#if os(Windows) - var path = value.path - if value.hasDirectoryPath && !path.hasSuffix("/") { path += "/" } - try path.encode(to: encoder) -#else +#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) } else { try value.path.encode(to: encoder) } +#elseif os(Linux) + try value.path(percentEncoded: false).encode(to: encoder) +#elseif os(Windows) + var path = value.path + if value.hasDirectoryPath && !path.hasSuffix("/") { path += "/" } + try path.encode(to: encoder) +#else +#error("Unsupported platform: Enigmatic supports Apple platforms, Linux, and Windows.") #endif } diff --git a/Sources/Enigmatic/Public/Codec/Strategies/Codec+UnixDate.swift b/Sources/Enigmatic/Public/Codec/Strategies/Codec+UnixDate.swift index 7178b0b..ffb5e35 100644 --- a/Sources/Enigmatic/Public/Codec/Strategies/Codec+UnixDate.swift +++ b/Sources/Enigmatic/Public/Codec/Strategies/Codec+UnixDate.swift @@ -4,7 +4,11 @@ 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) + @available(macOS 26, iOS 26, tvOS 26, watchOS 26, visionOS 26, *) +#else @available(anyAppleOS 26, *) +#endif public enum UnixDate: Hashable, DecodeStrategy, EncodeStrategy { /// Decodes scaled seconds since 1970 into a `Date`. /// - Throws: `DecodingError.dataCorrupted` for non-finite input or an invalid scale. diff --git a/Sources/Enigmatic/Public/Codec/Strategies/Codec+UnixIntDate.swift b/Sources/Enigmatic/Public/Codec/Strategies/Codec+UnixIntDate.swift index 2f3510d..b4e7ea1 100644 --- a/Sources/Enigmatic/Public/Codec/Strategies/Codec+UnixIntDate.swift +++ b/Sources/Enigmatic/Public/Codec/Strategies/Codec+UnixIntDate.swift @@ -4,7 +4,11 @@ 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) + @available(macOS 26, iOS 26, tvOS 26, watchOS 26, visionOS 26, *) +#else @available(anyAppleOS 26, *) +#endif public enum UnixIntDate: Hashable, DecodeStrategy, EncodeStrategy, Error { /// Decodes an integer scaled timestamp into a date. /// - Throws: `DecodingError.dataCorrupted` when the scale is invalid or the result is non-finite. diff --git a/Sources/Enigmatic/Public/Codec/Strategies/Codec+UnixIntMillisecondsDate.swift b/Sources/Enigmatic/Public/Codec/Strategies/Codec+UnixIntMillisecondsDate.swift index 0ea4fc6..d4bd718 100644 --- a/Sources/Enigmatic/Public/Codec/Strategies/Codec+UnixIntMillisecondsDate.swift +++ b/Sources/Enigmatic/Public/Codec/Strategies/Codec+UnixIntMillisecondsDate.swift @@ -3,7 +3,15 @@ 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) + @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. /// - Complexity: O(1); the operation performs a fixed number of numeric conversions and arithmetic operations. diff --git a/Sources/Enigmatic/Public/Codec/Strategies/Codec+UnixIntSecondsDate.swift b/Sources/Enigmatic/Public/Codec/Strategies/Codec+UnixIntSecondsDate.swift index 6983e20..3ac1093 100644 --- a/Sources/Enigmatic/Public/Codec/Strategies/Codec+UnixIntSecondsDate.swift +++ b/Sources/Enigmatic/Public/Codec/Strategies/Codec+UnixIntSecondsDate.swift @@ -3,7 +3,15 @@ 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) + @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. /// - Complexity: O(1); the operation performs a fixed number of numeric conversions and arithmetic operations. diff --git a/Sources/Enigmatic/Public/Codec/Strategies/Codec+UnixMillisecondsDate.swift b/Sources/Enigmatic/Public/Codec/Strategies/Codec+UnixMillisecondsDate.swift index 351ce27..ffae04e 100644 --- a/Sources/Enigmatic/Public/Codec/Strategies/Codec+UnixMillisecondsDate.swift +++ b/Sources/Enigmatic/Public/Codec/Strategies/Codec+UnixMillisecondsDate.swift @@ -3,7 +3,15 @@ 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) + @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. /// - Throws: `DecodingError.dataCorrupted` for non-finite input. diff --git a/Sources/Enigmatic/Public/Codec/Strategies/Codec+UnixSecondsDate.swift b/Sources/Enigmatic/Public/Codec/Strategies/Codec+UnixSecondsDate.swift index 228f2a4..454c0a6 100644 --- a/Sources/Enigmatic/Public/Codec/Strategies/Codec+UnixSecondsDate.swift +++ b/Sources/Enigmatic/Public/Codec/Strategies/Codec+UnixSecondsDate.swift @@ -3,7 +3,15 @@ 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) + @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. /// - Throws: `DecodingError.dataCorrupted` for non-finite input. diff --git a/Tests/EnigmaticTests/CodableTests.swift b/Tests/EnigmaticTests/CodableTests.swift index 3b587e5..89787ea 100644 --- a/Tests/EnigmaticTests/CodableTests.swift +++ b/Tests/EnigmaticTests/CodableTests.swift @@ -107,11 +107,13 @@ final class CodableTests: XCTestCase { func testAnyHashable() throws { let key1 = "café" let key2 = "cafe\u{301}" - #if os(anyAppleOS) || os(macOS) || os(iOS) || os(tvOS) || os(watchOS) || os(visionOS) +#if os(anyAppleOS) || os(macOS) || os(iOS) || os(tvOS) || os(watchOS) || os(visionOS) XCTAssertNotEqual(key1 as NSString, key2 as NSString) - #else +#elseif os(Linux) || os(Windows) XCTAssertEqual(key1 as NSString, key2 as NSString) - #endif +#else +#error("Unsupported platform: Enigmatic supports Apple platforms, Linux, and Windows.") +#endif XCTAssertEqual(key1, key2) let enigma1 = try Enigma(cast: [key1: 1]) let enigma2 = try Enigma(cast: [key2: 1]) @@ -174,7 +176,11 @@ final class CodableTests: XCTestCase { XCTAssertEqual(direct, indirect) } +#if compiler(<6.4) + @available(macOS 26, iOS 26, tvOS 26, watchOS 26, visionOS 26, *) +#else @available(anyAppleOS 26, *) +#endif func testUnixTime() throws { struct CodecDates: Codable, Equatable { @Codec.Box> diff --git a/Tests/EnigmaticTests/SerializationContractTests.swift b/Tests/EnigmaticTests/SerializationContractTests.swift index dac2ad2..a91b1d5 100644 --- a/Tests/EnigmaticTests/SerializationContractTests.swift +++ b/Tests/EnigmaticTests/SerializationContractTests.swift @@ -388,10 +388,10 @@ final class SerializationContractTests: XCTestCase { withUnsafeBytes(of: value) { bytes in #if os(anyAppleOS) || os(macOS) || os(iOS) || os(tvOS) || os(watchOS) || os(visionOS) NSValue(bytes: bytes.baseAddress!, objCType: type) -#else - // Corelibs Foundation cannot construct NSValue with t/T encodings. - // Supply the same encoding and bytes through NSValue's primitive API. +#elseif os(Linux) || os(Windows) BitInt128Value(bytes: bytes.baseAddress!, objCType: type) +#else +#error("Unsupported platform: Enigmatic supports Apple platforms, Linux, and Windows.") #endif } } @@ -589,7 +589,9 @@ final class SerializationContractTests: XCTestCase { } } -#if !(os(anyAppleOS) || os(macOS) || os(iOS) || os(tvOS) || os(watchOS) || os(visionOS)) +#if os(Linux) || os(Windows) +// Corelibs Foundation cannot construct NSValue with t/T encodings. +// Expose the encoding and bytes through NSValue's primitive API instead. private final class BitInt128Value: NSValue, @unchecked Sendable { private let storage: Data private let encoding: UnsafeMutablePointer diff --git a/Tests/EnigmaticTests/StrategyContractTests.swift b/Tests/EnigmaticTests/StrategyContractTests.swift index 915e680..a4fd833 100644 --- a/Tests/EnigmaticTests/StrategyContractTests.swift +++ b/Tests/EnigmaticTests/StrategyContractTests.swift @@ -741,7 +741,11 @@ final class StrategyContractTests: XCTestCase { } } +#if compiler(<6.4) + @available(macOS 26, iOS 26, tvOS 26, watchOS 26, visionOS 26, *) +#else @available(anyAppleOS 26, *) +#endif func testScaledDatesAndTruncation() throws { let date = Date(timeIntervalSince1970: -123.25) try roundTrip(Codec.UnixDate<0>.self, date, expected: .double(-123.25)) diff --git a/Tests/EnigmaticTests/Utility/ObjectiveC.swift b/Tests/EnigmaticTests/Utility/ObjectiveC.swift index d4af22f..e99cc41 100644 --- a/Tests/EnigmaticTests/Utility/ObjectiveC.swift +++ b/Tests/EnigmaticTests/Utility/ObjectiveC.swift @@ -1,4 +1,4 @@ -#if !(os(anyAppleOS) || os(macOS) || os(iOS) || os(tvOS) || os(watchOS) || os(visionOS)) +#if os(Linux) || os(Windows) @inline(__always) func autoreleasepool(_ body: () throws -> T) rethrows -> T { return try body()