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 @@
[](https://github.com/SwiftUtility/Enigmatic/actions/workflows/checks.yml)
[](https://github.com/SwiftUtility/Enigmatic/releases/latest)
-[](Package.swift)
+[](Package.swift)
[](#install)
[](LICENSE)
[](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()