From 820fc388a324f73bb7042c35becc2a5e82b9fe44 Mon Sep 17 00:00:00 2001 From: marcprux Date: Thu, 21 May 2026 14:08:27 -0400 Subject: [PATCH 1/3] Update package version to Swift 6.1 --- Package.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Package.swift b/Package.swift index 786062c..3ae4cdc 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version: 5.10 +// swift-tools-version: 6.1 import PackageDescription let package = Package( From 0a0f804857d8eb67d4866e42727637314a465b24 Mon Sep 17 00:00:00 2001 From: marcprux Date: Thu, 21 May 2026 15:35:29 -0400 Subject: [PATCH 2/3] Add Sendable annotations --- Sources/SkipDrive/SourceMap.swift | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/Sources/SkipDrive/SourceMap.swift b/Sources/SkipDrive/SourceMap.swift index b2d9844..e4cd59c 100644 --- a/Sources/SkipDrive/SourceMap.swift +++ b/Sources/SkipDrive/SourceMap.swift @@ -3,7 +3,7 @@ import Foundation /// A line and column in a particular source location. -public struct SourceLocation : Equatable { +public struct SourceLocation : Equatable, Sendable { public var path: String public var position: SourceLocation.Position @@ -14,7 +14,7 @@ public struct SourceLocation : Equatable { /// A line and column-based position in the source, appropriate for Xcode reporting. /// Line and column numbers start with 1 rather than 0. - public struct Position: Equatable, Comparable, Decodable { + public struct Position: Equatable, Comparable, Decodable, Sendable { public let line: Int public let column: Int @@ -64,28 +64,28 @@ public extension SourceLocation { } /// A decoded source map. This is the decodable counterpart to `SkipSyntax.OutputMap` -public struct SourceMap : Decodable { +public struct SourceMap : Decodable, Sendable { public let entries: [Entry] - public struct Entry : Decodable { + public struct Entry : Decodable, Sendable { public let sourceFile: Source.FilePath public let sourceRange: Source.Range? public let range: Source.Range } public struct Source { - public struct SourceLine : Decodable { + public struct SourceLine : Decodable, Sendable { public let offset: Int public let line: String } /// A Swift source file. - public struct FilePath: Hashable, Decodable { + public struct FilePath: Hashable, Decodable, Sendable { public let path: String } /// A line and column-based range in the source, appropriate for Xcode reporting. - public struct Range: Equatable, Decodable { + public struct Range: Equatable, Decodable, Sendable { public let start: SourceLocation.Position public let end: SourceLocation.Position } From 2410b1a288524524add0f0ba1af2e977300e66fa Mon Sep 17 00:00:00 2001 From: marcprux Date: Thu, 21 May 2026 16:21:21 -0400 Subject: [PATCH 3/3] Update concurrency --- Sources/SkipDrive/ToolSupport.swift | 34 ++++++++++++++++++----------- 1 file changed, 21 insertions(+), 13 deletions(-) diff --git a/Sources/SkipDrive/ToolSupport.swift b/Sources/SkipDrive/ToolSupport.swift index b7cc147..b358058 100644 --- a/Sources/SkipDrive/ToolSupport.swift +++ b/Sources/SkipDrive/ToolSupport.swift @@ -511,6 +511,12 @@ extension Range where Bound == Version { } } +/// Mutable reference cell for sharing optional read results between reader threads, +/// guarded externally by an NSLock so it is safe to cross the Sendable boundary. +fileprivate final class PendingResultBox: @unchecked Sendable { + var value: Result<[UInt8], Swift.Error>? +} + /// Process allows spawning new subprocesses and working with them. /// /// Note: This class is thread safe. @@ -532,7 +538,7 @@ extension Range where Bound == Version { case stream(stdout: OutputClosure, stderr: OutputClosure, redirectStderr: Bool) /// Default collect OutputRedirection that defaults to not redirect stderr. Provided for API compatibility. - public static let collect: OutputRedirection = .collect(redirectStderr: false) + nonisolated(unsafe) public static let collect: OutputRedirection = .collect(redirectStderr: false) /// Default stream OutputRedirection that defaults to not redirect stderr. Provided for API compatibility. public static func stream(stdout: @escaping OutputClosure, stderr: @escaping OutputClosure) -> Self { @@ -591,7 +597,7 @@ extension Range where Bound == Version { /// Typealias for logging handling closure public typealias LoggingHandler = (String) -> Void - private static var _loggingHandler: LoggingHandler? + nonisolated(unsafe) private static var _loggingHandler: LoggingHandler? private static let loggingHandlerLock = NSLock() /// Global logging handler. Use with care! preferably use instance level instead of setting one globally. @@ -707,7 +713,7 @@ extension Range where Bound == Version { /// /// Key: Executable name or path. /// Value: Path to the executable, if found. - private static var validatedExecutablesMap = [String: URL?]() + nonisolated(unsafe) private static var validatedExecutablesMap = [String: URL?]() private static let validatedExecutablesMapLock = NSLock() /// Create a new process instance. @@ -1050,10 +1056,12 @@ extension Range where Bound == Version { self.state = .outputReady(stdout: .success([]), stderr: .success([])) } } else { - var pending: Result<[UInt8], Swift.Error>? + let pending = PendingResultBox() let pendingLock = NSLock() let outputClosures = outputRedirection.outputClosures + let outputPipe = outputPipe + let stderrPipe = stderrPipe // Close the local write end of the output pipe. try close(fd: outputPipe[1]) @@ -1063,16 +1071,16 @@ extension Range where Bound == Version { let stdoutThread = Thread { [weak self] in if let readResult = self?.readOutput(onFD: outputPipe[0], outputClosure: outputClosures?.stdoutClosure) { pendingLock.withLock { - if let stderrResult = pending { + if let stderrResult = pending.value { self?.stateLock.withLock { self?.state = .outputReady(stdout: readResult, stderr: stderrResult) } } else { - pending = readResult + pending.value = readResult } } group.leave() - } else if let stderrResult = (pendingLock.withLock { pending }) { + } else if let stderrResult = (pendingLock.withLock { pending.value }) { // TODO: this is more of an error self?.stateLock.withLock { self?.state = .outputReady(stdout: .success([]), stderr: stderrResult) @@ -1092,16 +1100,16 @@ extension Range where Bound == Version { stderrThread = Thread { [weak self] in if let readResult = self?.readOutput(onFD: stderrPipe[0], outputClosure: outputClosures?.stderrClosure) { pendingLock.withLock { - if let stdoutResult = pending { + if let stdoutResult = pending.value { self?.stateLock.withLock { self?.state = .outputReady(stdout: stdoutResult, stderr: readResult) } } else { - pending = readResult + pending.value = readResult } } group.leave() - } else if let stdoutResult = (pendingLock.withLock { pending }) { + } else if let stdoutResult = (pendingLock.withLock { pending.value }) { // TODO: this is more of an error self?.stateLock.withLock { self?.state = .outputReady(stdout: stdoutResult, stderr: .success([])) @@ -1111,7 +1119,7 @@ extension Range where Bound == Version { } } else { pendingLock.withLock { - pending = .success([]) // no stderr in this case + pending.value = .success([]) // no stderr in this case } } @@ -2760,12 +2768,12 @@ public extension FileSystemError { /// Public stdout stream instance. -public var stdoutStream: ThreadSafeOutputByteStream = try! ThreadSafeOutputByteStream(LocalFileOutputByteStream( +nonisolated(unsafe) public var stdoutStream: ThreadSafeOutputByteStream = try! ThreadSafeOutputByteStream(LocalFileOutputByteStream( filePointer: stdout, closeOnDeinit: false)) /// Public stderr stream instance. -public var stderrStream: ThreadSafeOutputByteStream = try! ThreadSafeOutputByteStream(LocalFileOutputByteStream( +nonisolated(unsafe) public var stderrStream: ThreadSafeOutputByteStream = try! ThreadSafeOutputByteStream(LocalFileOutputByteStream( filePointer: stderr, closeOnDeinit: false))