From 690e400198050a2dc1f0d76f2d033a59a49dd9d2 Mon Sep 17 00:00:00 2001 From: Charles Srstka Date: Sun, 18 Oct 2026 14:05:03 -0400 Subject: [PATCH 1/3] Switch CI to shared workflow --- .github/workflows/ci.yml | 41 ++-------------------------------------- 1 file changed, 2 insertions(+), 39 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2f00557..02be10e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,42 +7,5 @@ on: branches: [ "main" ] jobs: - unit_tests: - strategy: - fail-fast: false - matrix: - os: [macos-latest, macos-15-intel, ubuntu-latest, ubuntu-24.04-arm] - traits: ["none", "Foundation"] - runs-on: ${{ matrix.os }} - - steps: - - uses: actions/checkout@v6 - - uses: SwiftyLab/setup-swift@latest - - name: Determine traits flags - id: traits - run: | - if [[ ${{ matrix.traits }} = 'all' ]]; then - echo "flags=--enable-all-traits" >> $GITHUB_OUTPUT - elif [[ ${{ matrix.traits }} = 'none' ]]; then - echo "flags=--disable-default-traits" >> $GITHUB_OUTPUT - else - echo "flags=--traits ${{ matrix.traits }}" >> $GITHUB_OUTPUT - fi - - name: Build - run: swift build --build-tests --enable-code-coverage -v ${{ steps.traits.outputs.flags }} - - name: Run tests - run: swift test --enable-code-coverage -v ${{ steps.traits.outputs.flags }} --xunit-output testresults.xml - - name: Collect artifacts - if: always() - run: | - mkdir -p artifacts - cp testresults*.xml artifacts/ 2>/dev/null || true - find .build -path "*/codecov/*.json" | xargs -I{} cp {} artifacts/ 2>/dev/null || true - - - name: Upload test results - uses: actions/upload-artifact@v7 - if: always() - with: - name: test-results-${{ matrix.os }}-${{ matrix.traits }} - path: artifacts/ - + run-ci: + uses: CharlesJS/shared-workflows/.github/workflows/ci.yml@main From b1a43df517a80121e9a7198c98f5c960095111d7 Mon Sep 17 00:00:00 2001 From: Charles Srstka Date: Sun, 18 Oct 2026 15:22:33 -0400 Subject: [PATCH 2/3] Detect CocoaErrors stored as NSError correctly on Linux+Foundation --- Sources/CSErrors/Error+CSErrors.swift | 27 ++++++++++++++++--------- Sources/CSErrors/NSError+CSErrors.swift | 11 ++-------- Tests/CSErrorsTests/NSErrorTests.swift | 10 ++++++++- 3 files changed, 29 insertions(+), 19 deletions(-) diff --git a/Sources/CSErrors/Error+CSErrors.swift b/Sources/CSErrors/Error+CSErrors.swift index a234d9a..7ed2a68 100644 --- a/Sources/CSErrors/Error+CSErrors.swift +++ b/Sources/CSErrors/Error+CSErrors.swift @@ -45,11 +45,9 @@ extension Error { return true } -#if Foundation && canImport(Darwin) - if let err = (self as NSError).toCSErrorProtocol(), err.isFileNotFoundError { + if let err = self.toCSErrorProtocol(), err.isFileNotFoundError { return true } -#endif return false } @@ -74,11 +72,9 @@ extension Error { return true } -#if Foundation && canImport(Darwin) - if let err = (self as NSError).toCSErrorProtocol(), err.isPermissionError { + if let err = self.toCSErrorProtocol(), err.isPermissionError { return true } -#endif return false } @@ -103,12 +99,25 @@ extension Error { return true } -#if Foundation && canImport(Darwin) - if let err = (self as NSError).toCSErrorProtocol(), err.isCancelledError { + if let err = self.toCSErrorProtocol(), err.isCancelledError { return true } -#endif return false } + + private func toCSErrorProtocol() -> (any CSErrorProtocol)? { +#if Foundation +#if canImport(Darwin) + (self as NSError).nserrorToCSErrorProtocol() +#else + switch self._domain { + case cocoaErrorDomain: CocoaError(CocoaError.Code(rawValue: self._code)) + default: nil + } +#endif +#else + nil +#endif + } } diff --git a/Sources/CSErrors/NSError+CSErrors.swift b/Sources/CSErrors/NSError+CSErrors.swift index e4e149c..6113e33 100644 --- a/Sources/CSErrors/NSError+CSErrors.swift +++ b/Sources/CSErrors/NSError+CSErrors.swift @@ -5,18 +5,11 @@ // Created by Charles Srstka on 11/11/23. // -#if Foundation && canImport(Darwin) - -#if canImport(FoundationEssentials) -import FoundationEssentials -#else +#if Foundation && !canImport(FoundationEssentials) import Foundation -#endif - -import System extension NSError { - internal func toCSErrorProtocol() -> (any CSErrorProtocol)? { + internal func nserrorToCSErrorProtocol() -> (any CSErrorProtocol)? { switch self.domain { case NSCocoaErrorDomain: return self as? CocoaError diff --git a/Tests/CSErrorsTests/NSErrorTests.swift b/Tests/CSErrorsTests/NSErrorTests.swift index 53c59c7..d84a63e 100644 --- a/Tests/CSErrorsTests/NSErrorTests.swift +++ b/Tests/CSErrorsTests/NSErrorTests.swift @@ -5,7 +5,7 @@ // Created by Charles Srstka on 11/7/23. // -#if Foundation && canImport(Darwin) +#if Foundation import Foundation import Testing @@ -16,11 +16,15 @@ struct NSErrorTests { func testFileNotFound() { #expect(NSError(domain: NSCocoaErrorDomain, code: NSFileNoSuchFileError).isFileNotFoundError) #expect(NSError(domain: NSCocoaErrorDomain, code: NSFileReadNoSuchFileError).isFileNotFoundError) +#if canImport(Darwin) #expect(NSError(domain: NSCocoaErrorDomain, code: NSUbiquitousFileUnavailableError).isFileNotFoundError) +#endif #expect(!NSError(domain: NSCocoaErrorDomain, code: NSFileWriteNoPermissionError).isFileNotFoundError) +#if canImport(Darwin) #expect(NSError(domain: NSURLErrorDomain, code: NSURLErrorFileDoesNotExist).isFileNotFoundError) #expect(!NSError(domain: NSURLErrorDomain, code: NSURLErrorNoPermissionsToReadFile).isFileNotFoundError) +#endif #expect(NSError(domain: NSOSStatusErrorDomain, code: -43 /* fnfErr */).isFileNotFoundError) #expect(!NSError(domain: NSOSStatusErrorDomain, code: -36 /* ioErr */).isFileNotFoundError) @@ -41,8 +45,10 @@ struct NSErrorTests { #expect(NSError(domain: NSCocoaErrorDomain, code: NSFileWriteNoPermissionError).isPermissionError) #expect(!NSError(domain: NSCocoaErrorDomain, code: NSFileNoSuchFileError).isPermissionError) +#if canImport(Darwin) #expect(NSError(domain: NSURLErrorDomain, code: NSURLErrorNoPermissionsToReadFile).isPermissionError) #expect(!NSError(domain: NSURLErrorDomain, code: NSURLErrorUnsupportedURL).isPermissionError) +#endif #expect(NSError(domain: NSOSStatusErrorDomain, code: -5000 /* afpAccessDenied */).isPermissionError) #expect(!NSError(domain: NSOSStatusErrorDomain, code: -43 /* fnfErr */).isPermissionError) @@ -63,9 +69,11 @@ struct NSErrorTests { #expect(NSError(domain: NSCocoaErrorDomain, code: NSUserCancelledError).isCancelledError) #expect(!NSError(domain: NSCocoaErrorDomain, code: NSFileNoSuchFileError).isCancelledError) +#if canImport(Darwin) #expect(NSError(domain: NSURLErrorDomain, code: NSURLErrorCancelled).isCancelledError) #expect(NSError(domain: NSURLErrorDomain, code: NSURLErrorUserCancelledAuthentication).isCancelledError) #expect(!NSError(domain: NSURLErrorDomain, code: NSURLErrorTimedOut).isCancelledError) +#endif #expect(NSError(domain: NSOSStatusErrorDomain, code: -128 /* userCanceledErr */).isCancelledError) #expect(NSError(domain: NSOSStatusErrorDomain, code: -1711 /* errAEWaitCanceled */).isCancelledError) From ab29ab2f0964e4d01c4f1645797aa606a8061187 Mon Sep 17 00:00:00 2001 From: Charles Srstka Date: Sun, 18 Oct 2026 15:22:48 -0400 Subject: [PATCH 3/3] Correctly map kPOSIXErrorBase errors on Linux --- Sources/CSErrors/OSStatus Errors.swift | 99 +++++++++++++++++++++++++- 1 file changed, 98 insertions(+), 1 deletion(-) diff --git a/Sources/CSErrors/OSStatus Errors.swift b/Sources/CSErrors/OSStatus Errors.swift index 30a09a3..677465a 100644 --- a/Sources/CSErrors/OSStatus Errors.swift +++ b/Sources/CSErrors/OSStatus Errors.swift @@ -14,6 +14,9 @@ import System #if canImport(Darwin) import Darwin #else +#if canImport(Glibc) +import Glibc +#endif public typealias OSStatus = Int32 #endif @@ -273,7 +276,101 @@ public func osStatusError( internal func translateOSStatusToPOSIX(_ code: some BinaryInteger) -> Int32? { let osStatus = OSStatus(code) - + +#if !canImport(Darwin) + // kPOSIXError* codes assume Darwin sys/errno.h, which can vary on other platforms + + switch osStatus { + case 100001: return EPERM + case 100002: return ENOENT + case 100003: return ESRCH + case 100004: return EINTR + case 100005: return EIO + case 100006: return ENXIO + case 100007: return E2BIG + case 100008: return ENOEXEC + case 100009: return EBADF + case 100010: return ECHILD + case 100011: return EDEADLK + case 100012: return ENOMEM + case 100013: return EACCES + case 100014: return EFAULT + case 100015: return ENOTBLK + case 100016: return EBUSY + case 100017: return EEXIST + case 100018: return EXDEV + case 100019: return ENODEV + case 100020: return ENOTDIR + case 100021: return EISDIR + case 100022: return EINVAL + case 100023: return ENFILE + case 100024: return EMFILE + case 100025: return ENOTTY + case 100026: return ETXTBSY + case 100027: return EFBIG + case 100028: return ENOSPC + case 100029: return ESPIPE + case 100030: return EROFS + case 100031: return EMLINK + case 100032: return EPIPE + case 100033: return EDOM + case 100034: return ERANGE + case 100035: return EAGAIN + case 100036: return EINPROGRESS + case 100037: return EALREADY + case 100038: return ENOTSOCK + case 100039: return EDESTADDRREQ + case 100040: return EMSGSIZE + case 100041: return EPROTOTYPE + case 100042: return ENOPROTOOPT + case 100043: return EPROTONOSUPPORT + case 100044: return ESOCKTNOSUPPORT + case 100045: return ENOTSUP + case 100046: return EPFNOSUPPORT + case 100047: return EAFNOSUPPORT + case 100048: return EADDRINUSE + case 100049: return EADDRNOTAVAIL + case 100050: return ENETDOWN + case 100051: return ENETUNREACH + case 100052: return ENETRESET + case 100053: return ECONNABORTED + case 100054: return ECONNRESET + case 100055: return ENOBUFS + case 100056: return EISCONN + case 100057: return ENOTCONN + case 100058: return ESHUTDOWN + case 100059: return ETOOMANYREFS + case 100060: return ETIMEDOUT + case 100061: return ECONNREFUSED + case 100062: return ELOOP + case 100063: return ENAMETOOLONG + case 100064: return EHOSTDOWN + case 100065: return EHOSTUNREACH + case 100066: return ENOTEMPTY + case 100068: return EUSERS + case 100069: return EDQUOT + case 100070: return ESTALE + case 100071: return EREMOTE + case 100077: return ENOLCK + case 100078: return ENOSYS + case 100084: return EOVERFLOW + case 100089: return ECANCELED + case 100090: return EIDRM + case 100091: return ENOMSG + case 100092: return EILSEQ + case 100094: return EBADMSG + case 100095: return EMULTIHOP + case 100096: return ENODATA + case 100097: return ENOLINK + case 100098: return ENOSR + case 100099: return ENOSTR + case 100100: return EPROTO + case 100101: return ETIME + case 100102: return EOPNOTSUPP + default: break + } +#endif + if ((OSStatusError.Codes.kPOSIXErrorBase + 1)..<(OSStatusError.Codes.kPOSIXErrorBase + 1000)).contains(osStatus) { return osStatus - OSStatusError.Codes.kPOSIXErrorBase }