Skip to content

Commit 15e0df9

Browse files
Fix the build on FreeBSD (and OpenBSD) (#152)
For now, we use Dispatch for AsyncIO and process termination monitoring, since BSDs (including macOS) all use kqueue as the Dispatch backend anyways. Closes #115
1 parent 7dc6e54 commit 15e0df9

24 files changed

+627
-493
lines changed

Package.swift

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,10 @@ var defaultTraits: Set<String> = ["SubprocessFoundation"]
2727
defaultTraits.insert("SubprocessSpan")
2828
#endif
2929

30+
let packageSwiftSettings: [SwiftSetting] = [
31+
.define("SUBPROCESS_ASYNCIO_DISPATCH", .when(platforms: [.macOS, .custom("freebsd"), .openbsd]))
32+
]
33+
3034
let package = Package(
3135
name: "Subprocess",
3236
platforms: [.macOS(.v13), .iOS("99.0")],
@@ -58,7 +62,7 @@ let package = Package(
5862
.enableExperimentalFeature("NonescapableTypes"),
5963
.enableExperimentalFeature("LifetimeDependence"),
6064
.enableExperimentalFeature("Span"),
61-
]
65+
] + packageSwiftSettings
6266
),
6367
.testTarget(
6468
name: "SubprocessTests",
@@ -70,7 +74,7 @@ let package = Package(
7074
],
7175
swiftSettings: [
7276
.enableExperimentalFeature("Span"),
73-
]
77+
] + packageSwiftSettings
7478
),
7579

7680
.target(

Sources/Subprocess/AsyncBufferSequence.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ public struct AsyncBufferSequence: AsyncSequence, @unchecked Sendable {
2323
public typealias Failure = any Swift.Error
2424
public typealias Element = Buffer
2525

26-
#if canImport(Darwin)
26+
#if SUBPROCESS_ASYNCIO_DISPATCH
2727
internal typealias DiskIO = DispatchIO
2828
#elseif canImport(WinSDK)
2929
internal typealias DiskIO = HANDLE
@@ -55,7 +55,7 @@ public struct AsyncBufferSequence: AsyncSequence, @unchecked Sendable {
5555
)
5656
guard let data else {
5757
// We finished reading. Close the file descriptor now
58-
#if canImport(Darwin)
58+
#if SUBPROCESS_ASYNCIO_DISPATCH
5959
try _safelyClose(.dispatchIO(self.diskIO))
6060
#elseif canImport(WinSDK)
6161
try _safelyClose(.handle(self.diskIO))
@@ -137,7 +137,7 @@ extension AsyncBufferSequence {
137137
self.eofReached = true
138138
return nil
139139
}
140-
#if canImport(Darwin)
140+
#if SUBPROCESS_ASYNCIO_DISPATCH
141141
// Unfortunately here we _have to_ copy the bytes out because
142142
// DispatchIO (rightfully) reuses buffer, which means `buffer.data`
143143
// has the same address on all iterations, therefore we can't directly

Sources/Subprocess/Buffer.swift

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
extension AsyncBufferSequence {
1818
/// A immutable collection of bytes
1919
public struct Buffer: Sendable {
20-
#if canImport(Darwin)
20+
#if SUBPROCESS_ASYNCIO_DISPATCH
2121
// We need to keep the backingData alive while Slice is alive
2222
internal let backingData: DispatchData
2323
internal let data: DispatchData.Region
@@ -45,7 +45,7 @@ extension AsyncBufferSequence {
4545
internal static func createFrom(_ data: [UInt8]) -> [Buffer] {
4646
return [.init(data: data)]
4747
}
48-
#endif // canImport(Darwin)
48+
#endif // SUBPROCESS_ASYNCIO_DISPATCH
4949
}
5050
}
5151

@@ -92,7 +92,7 @@ extension AsyncBufferSequence.Buffer {
9292

9393
// MARK: - Hashable, Equatable
9494
extension AsyncBufferSequence.Buffer: Equatable, Hashable {
95-
#if canImport(Darwin)
95+
#if SUBPROCESS_ASYNCIO_DISPATCH
9696
public static func == (lhs: AsyncBufferSequence.Buffer, rhs: AsyncBufferSequence.Buffer) -> Bool {
9797
return lhs.data == rhs.data
9898
}
@@ -104,7 +104,7 @@ extension AsyncBufferSequence.Buffer: Equatable, Hashable {
104104
// else Compiler generated conformances
105105
}
106106

107-
#if canImport(Darwin)
107+
#if SUBPROCESS_ASYNCIO_DISPATCH
108108
extension DispatchData.Region {
109109
static func == (lhs: DispatchData.Region, rhs: DispatchData.Region) -> Bool {
110110
return lhs.withUnsafeBytes { lhsBytes in
@@ -120,7 +120,7 @@ extension DispatchData.Region {
120120
}
121121
}
122122
}
123-
#if !SubprocessFoundation
123+
#if !canImport(Darwin) || !SubprocessFoundation
124124
/// `DispatchData.Region` is defined in Foundation, but we can't depend on Foundation when the SubprocessFoundation trait is disabled.
125125
extension DispatchData {
126126
typealias Region = _ContiguousBufferView

Sources/Subprocess/CMakeLists.txt

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ add_library(Subprocess
1717
Result.swift
1818
IO/Output.swift
1919
IO/Input.swift
20-
IO/AsyncIO+Darwin.swift
20+
IO/AsyncIO+Dispatch.swift
2121
IO/AsyncIO+Linux.swift
2222
IO/AsyncIO+Windows.swift
2323
Span+Subprocess.swift
@@ -36,8 +36,13 @@ elseif(LINUX OR ANDROID)
3636
Platforms/Subprocess+Unix.swift)
3737
elseif(APPLE)
3838
target_sources(Subprocess PRIVATE
39+
Platforms/Subprocess+BSD.swift
3940
Platforms/Subprocess+Darwin.swift
4041
Platforms/Subprocess+Unix.swift)
42+
elseif(FREEBSD OR OPENBSD)
43+
target_sources(Subprocess PRIVATE
44+
Platforms/Subprocess+BSD.swift
45+
Platforms/Subprocess+Unix.swift)
4146
endif()
4247

4348
target_compile_options(Subprocess PRIVATE

Sources/Subprocess/Configuration.swift

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -657,7 +657,7 @@ internal struct IODescriptor: ~Copyable {
657657
consuming func createIOChannel() -> IOChannel {
658658
let shouldClose = self.closeWhenDone
659659
self.closeWhenDone = false
660-
#if canImport(Darwin)
660+
#if SUBPROCESS_ASYNCIO_DISPATCH
661661
// Transferring out the ownership of fileDescriptor means we don't have go close here
662662
let closeFd = self.descriptor
663663
let dispatchIO: DispatchIO = DispatchIO(
@@ -708,10 +708,10 @@ internal struct IODescriptor: ~Copyable {
708708
}
709709

710710
internal struct IOChannel: ~Copyable, @unchecked Sendable {
711-
#if canImport(WinSDK)
712-
typealias Channel = HANDLE
713-
#elseif canImport(Darwin)
711+
#if SUBPROCESS_ASYNCIO_DISPATCH
714712
typealias Channel = DispatchIO
713+
#elseif canImport(WinSDK)
714+
typealias Channel = HANDLE
715715
#else
716716
typealias Channel = FileDescriptor
717717
#endif
@@ -733,10 +733,10 @@ internal struct IOChannel: ~Copyable, @unchecked Sendable {
733733
}
734734
closeWhenDone = false
735735

736-
#if canImport(WinSDK)
737-
try _safelyClose(.handle(self.channel))
738-
#elseif canImport(Darwin)
736+
#if SUBPROCESS_ASYNCIO_DISPATCH
739737
try _safelyClose(.dispatchIO(self.channel))
738+
#elseif canImport(WinSDK)
739+
try _safelyClose(.handle(self.channel))
740740
#else
741741
try _safelyClose(.fileDescriptor(self.channel))
742742
#endif

Sources/Subprocess/IO/AsyncIO+Darwin.swift renamed to Sources/Subprocess/IO/AsyncIO+Dispatch.swift

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
/// Darwin AsyncIO implementation based on DispatchIO
1313

1414
// MARK: - macOS (DispatchIO)
15-
#if canImport(Darwin)
15+
#if SUBPROCESS_ASYNCIO_DISPATCH
1616

1717
#if canImport(System)
1818
@preconcurrency import System
@@ -166,4 +166,8 @@ final class AsyncIO: Sendable {
166166
}
167167
}
168168

169+
#if !canImport(Darwin)
170+
extension DispatchData: @retroactive @unchecked Sendable { }
171+
#endif
172+
169173
#endif

Sources/Subprocess/IO/AsyncIO+Linux.swift

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111

1212
/// Linux AsyncIO implementation based on epoll
1313

14-
#if canImport(Glibc) || canImport(Android) || canImport(Musl)
14+
#if os(Linux) || os(Android)
1515

1616
#if canImport(System)
1717
@preconcurrency import System
@@ -266,6 +266,11 @@ final class AsyncIO: Sendable {
266266
targetEvent = EPOLL_EVENTS(EPOLLOUT)
267267
}
268268

269+
// Save the continuation (before calling epoll_ctl, so we don't miss any data)
270+
_registration.withLock { storage in
271+
storage[fileDescriptor.rawValue] = continuation
272+
}
273+
269274
var event = epoll_event(
270275
events: targetEvent.rawValue,
271276
data: epoll_data(fd: fileDescriptor.rawValue)
@@ -277,6 +282,10 @@ final class AsyncIO: Sendable {
277282
&event
278283
)
279284
if rc != 0 {
285+
_registration.withLock { storage in
286+
storage.removeValue(forKey: fileDescriptor.rawValue)
287+
}
288+
280289
let capturedError = errno
281290
let error = SubprocessError(
282291
code: .init(.asyncIOFailed(
@@ -287,10 +296,6 @@ final class AsyncIO: Sendable {
287296
continuation.finish(throwing: error)
288297
return
289298
}
290-
// Now save the continuation
291-
_registration.withLock { storage in
292-
storage[fileDescriptor.rawValue] = continuation
293-
}
294299
case .failure(let setupError):
295300
continuation.finish(throwing: setupError)
296301
return

Sources/Subprocess/IO/Output.swift

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,7 @@ public struct BytesOutput: OutputProtocol {
148148
internal func captureOutput(
149149
from diskIO: consuming IOChannel
150150
) async throws -> [UInt8] {
151-
#if canImport(Darwin)
151+
#if SUBPROCESS_ASYNCIO_DISPATCH
152152
var result: DispatchData? = nil
153153
#else
154154
var result: [UInt8]? = nil
@@ -173,7 +173,7 @@ public struct BytesOutput: OutputProtocol {
173173
underlyingError: nil
174174
)
175175
}
176-
#if canImport(Darwin)
176+
#if SUBPROCESS_ASYNCIO_DISPATCH
177177
return result?.array() ?? []
178178
#else
179179
return result ?? []
@@ -302,7 +302,7 @@ extension OutputProtocol {
302302
return try await bytesOutput.captureOutput(from: diskIO) as! Self.OutputType
303303
}
304304

305-
#if canImport(Darwin)
305+
#if SUBPROCESS_ASYNCIO_DISPATCH
306306
var result: DispatchData? = nil
307307
#else
308308
var result: [UInt8]? = nil
@@ -328,7 +328,7 @@ extension OutputProtocol {
328328
)
329329
}
330330

331-
#if canImport(Darwin)
331+
#if SUBPROCESS_ASYNCIO_DISPATCH
332332
return try self.output(from: result ?? .empty)
333333
#else
334334
return try self.output(from: result ?? [])
@@ -353,7 +353,7 @@ extension OutputProtocol where OutputType == Void {
353353

354354
#if SubprocessSpan
355355
extension OutputProtocol {
356-
#if canImport(Darwin)
356+
#if SUBPROCESS_ASYNCIO_DISPATCH
357357
internal func output(from data: DispatchData) throws -> OutputType {
358358
guard !data.isEmpty else {
359359
let empty = UnsafeRawBufferPointer(start: nil, count: 0)
@@ -380,7 +380,7 @@ extension OutputProtocol {
380380
return try self.output(from: span)
381381
}
382382
}
383-
#endif // canImport(Darwin)
383+
#endif // SUBPROCESS_ASYNCIO_DISPATCH
384384
}
385385
#endif
386386

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2025 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See https://swift.org/LICENSE.txt for license information
9+
//
10+
//===----------------------------------------------------------------------===//
11+
12+
#if os(macOS) || os(FreeBSD) || os(OpenBSD)
13+
14+
#if canImport(Darwin)
15+
import Darwin
16+
#elseif canImport(Glibc)
17+
import Glibc
18+
#endif
19+
20+
internal import Dispatch
21+
22+
// MARK: - Process Monitoring
23+
@Sendable
24+
internal func monitorProcessTermination(
25+
for processIdentifier: ProcessIdentifier
26+
) async throws -> TerminationStatus {
27+
switch Result(catching: { () throws(SubprocessError.UnderlyingError) -> TerminationStatus? in try processIdentifier.reap() }) {
28+
case let .success(status?):
29+
return status
30+
case .success(nil):
31+
break
32+
case let .failure(error):
33+
throw SubprocessError(
34+
code: .init(.failedToMonitorProcess),
35+
underlyingError: error
36+
)
37+
}
38+
return try await withCheckedThrowingContinuation { continuation in
39+
let source = DispatchSource.makeProcessSource(
40+
identifier: processIdentifier.value,
41+
eventMask: [.exit],
42+
queue: .global()
43+
)
44+
source.setEventHandler {
45+
source.cancel()
46+
continuation.resume(with: Result(catching: { () throws(SubprocessError.UnderlyingError) -> TerminationStatus in
47+
// NOTE_EXIT may be delivered slightly before the process becomes reapable,
48+
// so we must call waitid without WNOHANG to avoid a narrow possibility of a race condition.
49+
// If waitid does block, it won't do so for very long at all.
50+
try processIdentifier.blockingReap()
51+
}).mapError { underlyingError in
52+
SubprocessError(
53+
code: .init(.failedToMonitorProcess),
54+
underlyingError: underlyingError
55+
)
56+
})
57+
}
58+
source.resume()
59+
}
60+
}
61+
62+
#endif

Sources/Subprocess/Platforms/Subprocess+Darwin.swift

Lines changed: 0 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -497,46 +497,4 @@ extension ProcessIdentifier: CustomStringConvertible, CustomDebugStringConvertib
497497
public var debugDescription: String { "\(self.value)" }
498498
}
499499

500-
// MARK: - Process Monitoring
501-
@Sendable
502-
internal func monitorProcessTermination(
503-
for processIdentifier: ProcessIdentifier
504-
) async throws -> TerminationStatus {
505-
return try await withCheckedThrowingContinuation { continuation in
506-
let source = DispatchSource.makeProcessSource(
507-
identifier: processIdentifier.value,
508-
eventMask: [.exit],
509-
queue: .global()
510-
)
511-
source.setEventHandler {
512-
source.cancel()
513-
var siginfo = siginfo_t()
514-
let rc = waitid(P_PID, id_t(processIdentifier.value), &siginfo, WEXITED)
515-
guard rc == 0 else {
516-
continuation.resume(
517-
throwing: SubprocessError(
518-
code: .init(.failedToMonitorProcess),
519-
underlyingError: .init(rawValue: errno)
520-
)
521-
)
522-
return
523-
}
524-
switch siginfo.si_code {
525-
case .init(CLD_EXITED):
526-
continuation.resume(returning: .exited(siginfo.si_status))
527-
return
528-
case .init(CLD_KILLED), .init(CLD_DUMPED):
529-
continuation.resume(returning: .unhandledException(siginfo.si_status))
530-
case .init(CLD_TRAPPED), .init(CLD_STOPPED), .init(CLD_CONTINUED), .init(CLD_NOOP):
531-
// Ignore these signals because they are not related to
532-
// process exiting
533-
break
534-
default:
535-
fatalError("Unexpected exit status: \(siginfo.si_code)")
536-
}
537-
}
538-
source.resume()
539-
}
540-
}
541-
542500
#endif // canImport(Darwin)

0 commit comments

Comments
 (0)