Skip to content

Commit 9044073

Browse files
authored
Merge pull request #26 from jamf/ml_StrictConcurrency_minor_updates
Minor updates for StrictConcurrency and documentation updates
2 parents 2fdacca + f43f37c commit 9044073

14 files changed

+107
-49
lines changed

.github/workflows/build-and-test.yml

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,32 +13,32 @@ on:
1313
jobs:
1414
spm:
1515
name: SwiftPM build and test
16-
runs-on: macos-13
16+
runs-on: macos-14
1717
steps:
1818
- run: |
19-
sudo xcode-select -s /Applications/Xcode_15.0.app
19+
sudo xcode-select -s /Applications/Xcode_15.3.app
2020
- uses: actions/checkout@v3
2121
- name: Build swift packages
2222
run: swift build -v
2323
- name: Run tests
2424
run: swift test -v
2525
carthage:
2626
name: Xcode project build and test
27-
runs-on: macos-13
27+
runs-on: macos-14
2828
steps:
2929
- run: |
30-
sudo xcode-select -s /Applications/Xcode_15.0.app
30+
sudo xcode-select -s /Applications/Xcode_15.3.app
3131
- uses: actions/checkout@v3
3232
- name: Build xcode project
3333
run: xcodebuild build -scheme 'SubprocessMocks' -derivedDataPath .build
3434
- name: Run tests
3535
run: xcodebuild test -scheme 'Subprocess' -derivedDataPath .build
3636
cocoapods:
3737
name: Pod lib lint
38-
runs-on: macos-13
38+
runs-on: macos-14
3939
steps:
4040
- run: |
41-
sudo xcode-select -s /Applications/Xcode_15.0.app
41+
sudo xcode-select -s /Applications/Xcode_15.3.app
4242
- uses: actions/checkout@v3
4343
- name: Lib lint
4444
run: pod lib lint --verbose Subprocess.podspec --allow-warnings

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,12 @@ All notable changes to this project will be documented in this file.
77
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
88
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
99

10+
## 3.0.3 - 2024-04-15
11+
12+
### Changed
13+
- Correctly turned on `StrictConcurrency` in Swift 5.10 and earlier and added non-breaking conformance to `Sendable`.
14+
- Updated documentation for closure based usage where `nonisolated(unsafe)` is required to avoid an error in projects that use `StrictConcurrency`.
15+
1016
## 3.0.2 - 2024-02-07
1117

1218
### Added

Package.swift

Lines changed: 71 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,7 @@
1-
// swift-tools-version: 5.9
1+
// swift-tools-version: 5.10
22

33
import PackageDescription
44

5-
#if swift(<6)
6-
let swiftSettings: [SwiftSetting] = [
7-
.enableUpcomingFeature("StrictConcurrency"),
8-
.enableUpcomingFeature("ExistentialAny"),
9-
.enableUpcomingFeature("ForwardTrailingClosures"),
10-
.enableUpcomingFeature("ImplicitOpenExistentials"),
11-
.enableUpcomingFeature("BareSlashRegexLiterals"),
12-
.enableUpcomingFeature("ConciseMagicFile"),
13-
]
14-
#else
15-
let swiftSettings: [SwiftSetting] = []
16-
#endif
17-
185
let package = Package(
196
name: "Subprocess",
207
platforms: [ .macOS("10.15.4") ],
@@ -46,30 +33,92 @@ let package = Package(
4633
targets: [
4734
.target(
4835
name: "Subprocess",
49-
dependencies: [],
50-
swiftSettings: swiftSettings
36+
dependencies: []
5137
),
5238
.target(
5339
name: "SubprocessMocks",
5440
dependencies: [
5541
.target(name: "Subprocess")
56-
],
57-
swiftSettings: swiftSettings
42+
]
5843
),
5944
.testTarget(
6045
name: "UnitTests",
6146
dependencies: [
6247
.target(name: "Subprocess"),
6348
.target(name: "SubprocessMocks")
64-
],
65-
swiftSettings: swiftSettings
49+
]
6650
),
6751
.testTarget(
6852
name: "SystemTests",
6953
dependencies: [
7054
.target(name: "Subprocess")
71-
],
72-
swiftSettings: swiftSettings
55+
]
7356
)
7457
]
7558
)
59+
60+
for target in package.targets {
61+
var swiftSettings = target.swiftSettings ?? []
62+
63+
// According to Swift's piecemeal adoption plan features that were
64+
// upcoming features that become language defaults and are still enabled
65+
// as upcoming features will result in a compiler error. Currently in the
66+
// latest 5.10 compiler this doesn't happen, the compiler ignores it.
67+
//
68+
// If the situation does change and enabling default language features
69+
// does result in an error in future versions we attempt to guard against
70+
// this by using the hasFeature(x) compiler directive to see if we have a
71+
// feature already, or if we can enable it. It's safe to enable features
72+
// that don't exist in older compiler versions as the compiler will ignore
73+
// features it doesn't have implemented.
74+
75+
// swift 6
76+
#if !hasFeature(ConciseMagicFile)
77+
swiftSettings.append(.enableUpcomingFeature("ConciseMagicFile"))
78+
#endif
79+
80+
#if !hasFeature(ForwardTrailingClosures)
81+
swiftSettings.append(.enableUpcomingFeature("ForwardTrailingClosures"))
82+
#endif
83+
84+
#if !hasFeature(StrictConcurrency)
85+
swiftSettings.append(.enableUpcomingFeature("StrictConcurrency"))
86+
// StrictConcurrency is under experimental features in Swift <=5.10 contrary to some posts and documentation
87+
swiftSettings.append(.enableExperimentalFeature("StrictConcurrency"))
88+
#endif
89+
90+
#if !hasFeature(BareSlashRegexLiterals)
91+
swiftSettings.append(.enableUpcomingFeature("BareSlashRegexLiterals"))
92+
#endif
93+
94+
#if !hasFeature(ImplicitOpenExistentials)
95+
swiftSettings.append(.enableUpcomingFeature("ImplicitOpenExistentials"))
96+
#endif
97+
98+
#if !hasFeature(ImportObjcForwardDeclarations)
99+
swiftSettings.append(.enableUpcomingFeature("ImportObjcForwardDeclarations"))
100+
#endif
101+
102+
#if !hasFeature(DisableOutwardActorInference)
103+
swiftSettings.append(.enableUpcomingFeature("DisableOutwardActorInference"))
104+
#endif
105+
106+
#if !hasFeature(InternalImportsByDefault)
107+
swiftSettings.append(.enableUpcomingFeature("InternalImportsByDefault"))
108+
#endif
109+
110+
#if !hasFeature(IsolatedDefaultValues)
111+
swiftSettings.append(.enableUpcomingFeature("IsolatedDefaultValues"))
112+
#endif
113+
114+
#if !hasFeature(GlobalConcurrency)
115+
swiftSettings.append(.enableUpcomingFeature("GlobalConcurrency"))
116+
#endif
117+
118+
// swift 7
119+
#if !hasFeature(ExistentialAny)
120+
swiftSettings.append(.enableUpcomingFeature("ExistentialAny"))
121+
#endif
122+
123+
target.swiftSettings = swiftSettings
124+
}

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,12 +137,16 @@ if process.exitCode == 0 {
137137
```swift
138138
let command: [String] = ...
139139
let process = Subprocess(command)
140+
nonisolated(unsafe) var outputData: Data?
141+
nonisolated(unsafe) var errorData: Data?
140142

141143
// The outputHandler and errorHandler are invoked serially
142144
try process.launch(outputHandler: { data in
143145
// Handle new data read from stdout
146+
outputData = data
144147
}, errorHandler: { data in
145148
// Handle new data read from stderr
149+
errorData = data
146150
}, terminationHandler: { process in
147151
// Handle process termination, all scheduled calls to
148152
// the outputHandler and errorHandler are guaranteed to

Sources/Subprocess/Pipe+AsyncBytes.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ extension Pipe {
3838

3939
public func makeAsyncIterator() -> AsyncStream<Element>.Iterator {
4040
AsyncStream { continuation in
41-
pipe.fileHandleForReading.readabilityHandler = { handle in
41+
pipe.fileHandleForReading.readabilityHandler = { @Sendable handle in
4242
let availableData = handle.availableData
4343

4444
guard !availableData.isEmpty else {

Sources/Subprocess/Shell.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ import Foundation
3232
public class Shell {
3333

3434
/// OptionSet representing output handling
35-
public struct OutputOptions: OptionSet {
35+
public struct OutputOptions: OptionSet, Sendable {
3636
public let rawValue: Int
3737

3838
/// Processes data written to stdout

Sources/Subprocess/Subprocess.swift

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ import Combine
3131
/// Class used for asynchronous process execution
3232
public class Subprocess: @unchecked Sendable {
3333
/// Output options.
34-
public struct OutputOptions: OptionSet {
34+
public struct OutputOptions: OptionSet, Sendable {
3535
public let rawValue: Int
3636

3737
/// Buffer standard output.
@@ -118,7 +118,7 @@ public class Subprocess: @unchecked Sendable {
118118
///
119119
/// It is the callers responsibility to ensure that any reads occur if waiting for the process to exit otherwise a deadlock can happen if the process is waiting to write to its output buffer.
120120
/// A task group can be used to wait for exit while reading the output. If the output is discardable consider passing (`[]`) an empty set for the options which effectively flushes output to null.
121-
public func run(options: OutputOptions = [.standardOutput, .standardError]) throws -> (standardOutput: Pipe.AsyncBytes, standardError: Pipe.AsyncBytes, waitUntilExit: () async -> Void) {
121+
public func run(options: OutputOptions = [.standardOutput, .standardError]) throws -> (standardOutput: Pipe.AsyncBytes, standardError: Pipe.AsyncBytes, waitUntilExit: @Sendable () async -> Void) {
122122
let standardOutput: Pipe.AsyncBytes = {
123123
if options.contains(.standardOutput) {
124124
let pipe = Pipe()
@@ -162,7 +162,7 @@ public class Subprocess: @unchecked Sendable {
162162
}
163163
}
164164
}
165-
let waitUntilExit = {
165+
let waitUntilExit = { @Sendable in
166166
await task.value
167167
}
168168

@@ -216,7 +216,7 @@ public class Subprocess: @unchecked Sendable {
216216
/// }
217217
/// }
218218
///
219-
public func run<Input>(standardInput: Input, options: OutputOptions = [.standardOutput, .standardError]) throws -> (standardOutput: Pipe.AsyncBytes, standardError: Pipe.AsyncBytes, waitUntilExit: () async -> Void) where Input : AsyncSequence, Input.Element == UInt8 {
219+
public func run<Input>(standardInput: Input, options: OutputOptions = [.standardOutput, .standardError]) throws -> (standardOutput: Pipe.AsyncBytes, standardError: Pipe.AsyncBytes, waitUntilExit: @Sendable () async -> Void) where Input : AsyncSequence, Input.Element == UInt8 {
220220
process.standardInput = try SubprocessDependencyBuilder.shared.makeInputPipe(sequence: standardInput)
221221
return try run(options: options)
222222
}

Sources/Subprocess/SubprocessDependencyBuilder.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ public protocol SubprocessDependencyFactory {
5252
/// Default implementation of SubprocessDependencyFactory
5353
public struct SubprocessDependencyBuilder: SubprocessDependencyFactory {
5454
private static let queue = DispatchQueue(label: "\(Self.self)")
55-
private static var _shared: any SubprocessDependencyFactory = SubprocessDependencyBuilder()
55+
nonisolated(unsafe) private static var _shared: any SubprocessDependencyFactory = SubprocessDependencyBuilder()
5656
/// Shared instance used for dependency creation
5757
public static var shared: any SubprocessDependencyFactory {
5858
get {
@@ -80,7 +80,7 @@ public struct SubprocessDependencyBuilder: SubprocessDependencyFactory {
8080
return try FileHandle(forReadingFrom: url)
8181
}
8282

83-
public func makeInputPipe<Input>(sequence: Input) throws -> Pipe where Input : AsyncSequence, Input.Element == UInt8 {
83+
public func makeInputPipe<Input>(sequence: Input) throws -> Pipe where Input : AsyncSequence & Sendable, Input.Element == UInt8 {
8484
let pipe = Pipe()
8585
// see here: https://developer.apple.com/forums/thread/690382
8686
let result = fcntl(pipe.fileHandleForWriting.fileDescriptor, F_SETNOSIGPIPE, 1)

Sources/SubprocessMocks/MockOutput.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828
import Foundation
2929

3030
/// A way to supply data to mock methods
31-
public protocol MockOutput {
31+
public protocol MockOutput: Sendable {
3232
var data: Data { get }
3333
}
3434

Sources/SubprocessMocks/MockProcess.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ import Subprocess
3131
#endif
3232

3333
/// Interface used for mocking a process
34-
public struct MockProcess {
34+
public struct MockProcess: Sendable {
3535

3636
/// The underlying `MockProcessReference`
3737
public var reference: MockProcessReference
@@ -100,7 +100,7 @@ open class MockProcessReference: Process {
100100

101101
/// Creates a new `MockProcessReference` calling run stub block
102102
/// - Parameter block: Block used to stub `Process.run`
103-
public init(withRunBlock block: @escaping (MockProcess) -> Void) {
103+
public init(withRunBlock block: @escaping @Sendable (MockProcess) -> Void) {
104104
context = Context(runStub: { mock in
105105
Task(priority: .userInitiated) {
106106
block(mock)

Sources/SubprocessMocks/MockSubprocess.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ public extension Subprocess {
5151
/// - Parameters:
5252
/// - command: The command to mock
5353
/// - runBlock: Block called with a `MockProcess` to mock process execution.
54-
static func stub(_ command: [String], runBlock: ((MockProcess) -> Void)? = nil) {
54+
static func stub(_ command: [String], runBlock: (@Sendable (MockProcess) -> Void)? = nil) {
5555
let mock = MockProcessReference(withRunBlock: runBlock ?? { $0.exit() })
5656
MockSubprocessDependencyBuilder.shared.stub(command, process: mock)
5757
}
@@ -116,7 +116,7 @@ public extension Subprocess {
116116
/// - file: Source file where expect was called (Default: #file)
117117
/// - line: Line number of source file where expect was called (Default: #line)
118118
/// - runBlock: Block called with a `MockProcess` to mock process execution
119-
static func expect(_ command: [String], input: Input? = nil, file: StaticString = #file, line: UInt = #line, runBlock: ((MockProcess) -> Void)? = nil) {
119+
static func expect(_ command: [String], input: Input? = nil, file: StaticString = #file, line: UInt = #line, runBlock: (@Sendable (MockProcess) -> Void)? = nil) {
120120
let mock = MockProcessReference(withRunBlock: runBlock ?? { $0.exit() })
121121
MockSubprocessDependencyBuilder.shared.expect(command, input: input, process: mock, file: file, line: line)
122122
}

Sources/SubprocessMocks/MockSubprocessDependencyBuilder.swift

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,6 @@ public final class MockPipe: Pipe {
8888
}
8989

9090
class MockSubprocessDependencyBuilder {
91-
9291
class MockItem {
9392
var used = false
9493
var command: [String]
@@ -108,7 +107,7 @@ class MockSubprocessDependencyBuilder {
108107

109108
var mocks: [MockItem] = []
110109

111-
static let shared = MockSubprocessDependencyBuilder()
110+
nonisolated(unsafe) static let shared = MockSubprocessDependencyBuilder()
112111

113112
init() { SubprocessDependencyBuilder.shared = self }
114113

@@ -242,7 +241,7 @@ extension MockSubprocessDependencyBuilder: SubprocessDependencyFactory {
242241
return handle
243242
}
244243

245-
func makeInputPipe<Input>(sequence: Input) throws -> Pipe where Input : AsyncSequence, Input.Element == UInt8 {
244+
func makeInputPipe<Input>(sequence: Input) throws -> Pipe where Input : AsyncSequence & Sendable, Input.Element == UInt8 {
246245
let semaphore = DispatchSemaphore(value: 0)
247246
let pipe = MockPipe()
248247

Tests/SystemTests/SubprocessSystemTests.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ final class SubprocessSystemTests: XCTestCase {
8686
switch line {
8787
case "hello":
8888
Task {
89-
input.yield("world\n")
89+
_ = input.yield("world\n")
9090
}
9191
case "world":
9292
input.yield("and\nuniverse")

Tests/UnitTests/SubprocessTests.swift

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ final class SubprocessTests: XCTestCase {
103103
func testGetPID() throws {
104104
// Given
105105
let mockCalled = expectation(description: "Mock setup called")
106-
var expectedPID: Int32?
106+
nonisolated(unsafe) var expectedPID: Int32?
107107
Subprocess.expect(command) { mock in
108108
expectedPID = mock.reference.processIdentifier
109109
mockCalled.fulfill()
@@ -185,7 +185,7 @@ final class SubprocessTests: XCTestCase {
185185

186186
// MARK: suspend
187187

188-
func testSuspend() throws {
188+
@MainActor func testSuspend() throws {
189189
// Given
190190
let semaphore = DispatchSemaphore(value: 0)
191191
let suspendCalled = expectation(description: "Suspend called")
@@ -210,7 +210,7 @@ final class SubprocessTests: XCTestCase {
210210

211211
// MARK: resume
212212

213-
func testResume() throws {
213+
@MainActor func testResume() throws {
214214
// Given
215215
let semaphore = DispatchSemaphore(value: 0)
216216
let resumeCalled = expectation(description: "Resume called")
@@ -235,7 +235,7 @@ final class SubprocessTests: XCTestCase {
235235

236236
// MARK: kill
237237

238-
func testKill() throws {
238+
@MainActor func testKill() throws {
239239
// Given
240240
let semaphore = DispatchSemaphore(value: 0)
241241
let terminateCalled = expectation(description: "Terminate called")

0 commit comments

Comments
 (0)