Skip to content

Commit ad63fd6

Browse files
committed
Add support for FreeBSD
Also includes speculative support for OpenBSD, which I haven't tested, but which is likely correct as it tends to require the same code paths as FreeBSD.
1 parent f835957 commit ad63fd6

File tree

7 files changed

+59
-26
lines changed

7 files changed

+59
-26
lines changed

Package.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ var dep: [Package.Dependency] = [
1313
dep.append(
1414
.package(
1515
url: "https://github.com/apple/swift-docc-plugin",
16-
from: "1.4.3"
16+
from: "1.4.5"
1717
),
1818
)
1919
#endif

Sources/Subprocess/Platforms/Subprocess+Linux.swift

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -285,8 +285,14 @@ internal func monitorProcessTermination(
285285

286286
// Small helper to provide thread-safe access to the child process to continuations map as well as a condition variable to suspend the calling thread when there are no subprocesses to wait for. Note that Mutex cannot be used here because we need the semantics of pthread_cond_wait, which requires passing the pthread_mutex_t instance as a parameter, something the Mutex API does not provide access to.
287287
private final class ChildProcessContinuations: Sendable {
288+
#if os(FreeBSD) || os(OpenBSD)
289+
typealias MutexType = pthread_mutex_t?
290+
#else
291+
typealias MutexType = pthread_mutex_t
292+
#endif
293+
288294
private nonisolated(unsafe) var continuations = [pid_t: CheckedContinuation<TerminationStatus, any Error>]()
289-
private nonisolated(unsafe) let mutex = UnsafeMutablePointer<pthread_mutex_t>.allocate(capacity: 1)
295+
private nonisolated(unsafe) let mutex = UnsafeMutablePointer<MutexType>.allocate(capacity: 1)
290296

291297
init() {
292298
pthread_mutex_init(mutex, nil)
@@ -298,7 +304,7 @@ private final class ChildProcessContinuations: Sendable {
298304
}
299305
}
300306

301-
func withUnsafeUnderlyingLock<R>(_ body: (UnsafeMutablePointer<pthread_mutex_t>, inout [pid_t: CheckedContinuation<TerminationStatus, any Error>]) throws -> R) rethrows -> R {
307+
func withUnsafeUnderlyingLock<R>(_ body: (UnsafeMutablePointer<MutexType>, inout [pid_t: CheckedContinuation<TerminationStatus, any Error>]) throws -> R) rethrows -> R {
302308
pthread_mutex_lock(mutex)
303309
defer {
304310
pthread_mutex_unlock(mutex)
@@ -310,11 +316,16 @@ private final class ChildProcessContinuations: Sendable {
310316
private let _childProcessContinuations = ChildProcessContinuations()
311317

312318
private nonisolated(unsafe) let _waitThreadNoChildrenCondition = {
319+
#if os(FreeBSD) || os(OpenBSD)
320+
let result = UnsafeMutablePointer<pthread_cond_t?>.allocate(capacity: 1)
321+
#else
313322
let result = UnsafeMutablePointer<pthread_cond_t>.allocate(capacity: 1)
323+
#endif
314324
_ = pthread_cond_init(result, nil)
315325
return result
316326
}()
317327

328+
#if !os(FreeBSD) && !os(OpenBSD)
318329
private extension siginfo_t {
319330
var si_status: Int32 {
320331
#if canImport(Glibc)
@@ -336,11 +347,16 @@ private extension siginfo_t {
336347
#endif
337348
}
338349
}
350+
#endif
339351

340352
private let setup: () = {
341353
// Create the thread. It will run immediately; because it runs in an infinite
342354
// loop, we aren't worried about detaching or joining it.
355+
#if os(FreeBSD) || os(OpenBSD)
356+
var thread: pthread_t?
357+
#else
343358
var thread = pthread_t()
359+
#endif
344360
_ = pthread_create(
345361
&thread,
346362
nil,

Sources/Subprocess/Platforms/Subprocess+Unix.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -176,10 +176,10 @@ extension Environment {
176176
/// length = `key` + `=` + `value` + `\null`
177177
let totalLength = keyContainer.count + 1 + valueContainer.count + 1
178178
let fullString: UnsafeMutablePointer<CChar> = .allocate(capacity: totalLength)
179-
#if canImport(Darwin)
180-
_ = snprintf(ptr: fullString, totalLength, "%s=%s", rawByteKey, rawByteValue)
181-
#else
179+
#if os(Linux)
182180
_ = _shims_snprintf(fullString, CInt(totalLength), "%s=%s", rawByteKey, rawByteValue)
181+
#else
182+
_ = snprintf(ptr: fullString, totalLength, "%s=%s", rawByteKey, rawByteValue)
183183
#endif
184184
return fullString
185185
}

Sources/_SubprocessCShims/process_shims.c

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -298,7 +298,7 @@ int _subprocess_spawn(
298298
#endif // TARGET_OS_MAC
299299

300300
// MARK: - Linux (fork/exec + posix_spawn fallback)
301-
#if TARGET_OS_LINUX
301+
#if TARGET_OS_LINUX || TARGET_OS_BSD
302302
#ifndef __GLIBC_PREREQ
303303
#define __GLIBC_PREREQ(maj, min) 0
304304
#endif

Tests/SubprocessTests/SubprocessTests+Linux.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
//
1010
//===----------------------------------------------------------------------===//
1111

12-
#if canImport(Glibc) || canImport(Bionic) || canImport(Musl)
12+
#if os(Linux) || os(Android)
1313

1414
#if canImport(Bionic)
1515
import Bionic

Tests/SubprocessTests/SubprocessTests+Unix.swift

Lines changed: 23 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ extension SubprocessUnixTests {
102102
extension SubprocessUnixTests {
103103
@Test func testArgumentsArrayLiteral() async throws {
104104
let result = try await Subprocess.run(
105-
.path("/bin/bash"),
105+
.path("/bin/sh"),
106106
arguments: ["-c", "echo Hello World!"],
107107
output: .string
108108
)
@@ -117,7 +117,7 @@ extension SubprocessUnixTests {
117117

118118
@Test func testArgumentsOverride() async throws {
119119
let result = try await Subprocess.run(
120-
.path("/bin/bash"),
120+
.path("/bin/sh"),
121121
arguments: .init(
122122
executablePathOverride: "apple",
123123
remainingValues: ["-c", "echo $0"]
@@ -157,7 +157,7 @@ extension SubprocessUnixTests {
157157
extension SubprocessUnixTests {
158158
@Test func testEnvironmentInherit() async throws {
159159
let result = try await Subprocess.run(
160-
.path("/bin/bash"),
160+
.path("/bin/sh"),
161161
arguments: ["-c", "printenv PATH"],
162162
environment: .inherit,
163163
output: .string
@@ -173,7 +173,7 @@ extension SubprocessUnixTests {
173173

174174
@Test func testEnvironmentInheritOverride() async throws {
175175
let result = try await Subprocess.run(
176-
.path("/bin/bash"),
176+
.path("/bin/sh"),
177177
arguments: ["-c", "printenv HOME"],
178178
environment: .inherit.updating([
179179
"HOME": "/my/new/home"
@@ -595,7 +595,7 @@ extension SubprocessUnixTests {
595595
contentsOf: URL(filePath: theMysteriousIsland.string)
596596
)
597597
let catResult = try await Subprocess.run(
598-
.path("/bin/bash"),
598+
.path("/bin/sh"),
599599
arguments: ["-c", "cat \(theMysteriousIsland.string) 1>&2"],
600600
error: .data(limit: 2048 * 1024)
601601
)
@@ -668,11 +668,14 @@ extension SubprocessUnixTests {
668668
var platformOptions = PlatformOptions()
669669
platformOptions.supplementaryGroups = Array(expectedGroups)
670670
let idResult = try await Subprocess.run(
671-
.path("/usr/bin/swift"),
671+
.name("swift"),
672672
arguments: [getgroupsSwift.string],
673673
platformOptions: platformOptions,
674-
output: .string
674+
output: .string,
675+
error: .string,
675676
)
677+
let error = try #require(idResult.standardError)
678+
try #require(error == "")
676679
#expect(idResult.terminationStatus.isSuccess)
677680
let ids = try #require(
678681
idResult.standardOutput
@@ -696,7 +699,7 @@ extension SubprocessUnixTests {
696699
// Sets the process group ID to 0, which creates a new session
697700
platformOptions.processGroupID = 0
698701
let psResult = try await Subprocess.run(
699-
.path("/bin/bash"),
702+
.path("/bin/sh"),
700703
arguments: ["-c", "ps -o pid,pgid -p $$"],
701704
platformOptions: platformOptions,
702705
output: .string
@@ -723,22 +726,22 @@ extension SubprocessUnixTests {
723726
// Check the process ID (pid), process group ID (pgid), and
724727
// controlling terminal's process group ID (tpgid)
725728
let psResult = try await Subprocess.run(
726-
.path("/bin/bash"),
729+
.path("/bin/sh"),
727730
arguments: ["-c", "ps -o pid,pgid,tpgid -p $$"],
728731
platformOptions: platformOptions,
729732
output: .string
730733
)
731734
try assertNewSessionCreated(with: psResult)
732735
}
733736

734-
@Test func testTeardownSequence() async throws {
737+
@Test(.requiresBash) func testTeardownSequence() async throws {
735738
let result = try await Subprocess.run(
736-
.path("/bin/bash"),
739+
.name("bash"),
737740
arguments: [
738741
"-c",
739742
"""
740743
set -e
741-
trap 'echo saw SIGQUIT;' SIGQUIT
744+
trap 'echo saw SIGQUIT;' QUIT
742745
trap 'echo saw SIGTERM;' TERM
743746
trap 'echo saw SIGINT; exit 42;' INT
744747
while true; do sleep 1; done
@@ -777,7 +780,7 @@ extension SubprocessUnixTests {
777780
@Test func testRunDetached() async throws {
778781
let (readFd, writeFd) = try FileDescriptor.pipe()
779782
let pid = try runDetached(
780-
.path("/bin/bash"),
783+
.path("/bin/sh"),
781784
arguments: ["-c", "echo $$"],
782785
output: writeFd
783786
)
@@ -1046,11 +1049,11 @@ extension FileDescriptor {
10461049

10471050
// MARK: - Performance Tests
10481051
extension SubprocessUnixTests {
1049-
@Test func testConcurrentRun() async throws {
1052+
@Test(.requiresBash) func testConcurrentRun() async throws {
10501053
// Launch as many processes as we can
10511054
// Figure out the max open file limit
10521055
let limitResult = try await Subprocess.run(
1053-
.path("/bin/bash"),
1056+
.path("/bin/sh"),
10541057
arguments: ["-c", "ulimit -n"],
10551058
output: .string
10561059
)
@@ -1075,8 +1078,9 @@ extension SubprocessUnixTests {
10751078
let byteCount = 1000
10761079
for _ in 0..<maxConcurrent {
10771080
group.addTask {
1081+
// This invocation specifically requires bash semantics; sh (on FreeBSD at least) does not consistently support -s in this way
10781082
let r = try await Subprocess.run(
1079-
.path("/bin/bash"),
1083+
.name("bash"),
10801084
arguments: [
10811085
"-sc", #"echo "$1" && echo "$1" >&2"#, "--", String(repeating: "X", count: byteCount),
10821086
],
@@ -1099,13 +1103,14 @@ extension SubprocessUnixTests {
10991103
}
11001104
}
11011105

1102-
@Test func testCaptureLongStandardOutputAndError() async throws {
1106+
@Test(.requiresBash) func testCaptureLongStandardOutputAndError() async throws {
11031107
try await withThrowingTaskGroup(of: Void.self) { group in
11041108
var running = 0
11051109
for _ in 0..<10 {
11061110
group.addTask {
1111+
// This invocation specifically requires bash semantics; sh (on FreeBSD at least) does not consistently support -s in this way
11071112
let r = try await Subprocess.run(
1108-
.path("/bin/bash"),
1113+
.name("bash"),
11091114
arguments: [
11101115
"-sc", #"echo "$1" && echo "$1" >&2"#, "--", String(repeating: "X", count: 100_000),
11111116
],

Tests/SubprocessTests/TestSupport.swift

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ import Foundation
1717
import FoundationEssentials
1818
#endif
1919

20+
import Testing
21+
import Subprocess
22+
2023
internal func randomString(length: Int, lettersOnly: Bool = false) -> String {
2124
let letters: String
2225
if lettersOnly {
@@ -42,3 +45,12 @@ internal func directory(_ lhs: String, isSameAs rhs: String) -> Bool {
4245

4346
return canonicalLhs == canonicalRhs
4447
}
48+
49+
extension Trait where Self == ConditionTrait {
50+
public static var requiresBash: Self {
51+
enabled(
52+
if: (try? Executable.name("bash").resolveExecutablePath(in: .inherit)) != nil,
53+
"This test requires bash (install `bash` package on Linux/BSD)"
54+
)
55+
}
56+
}

0 commit comments

Comments
 (0)