Skip to content

Add support for FreeBSD #99

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jun 30, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ var dep: [Package.Dependency] = [
dep.append(
.package(
url: "https://github.com/apple/swift-docc-plugin",
from: "1.4.3"
from: "1.4.5"
),
)
#endif
Expand Down
20 changes: 18 additions & 2 deletions Sources/Subprocess/Platforms/Subprocess+Linux.swift
Original file line number Diff line number Diff line change
Expand Up @@ -285,8 +285,14 @@ internal func monitorProcessTermination(

// 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.
private final class ChildProcessContinuations: Sendable {
#if os(FreeBSD) || os(OpenBSD)
typealias MutexType = pthread_mutex_t?
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is unfortunate. Why would we support an optional lock?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For Linux/Glibc, pthread_mutex_t is a union, but on OpenBSD/FreeBSD, it is a pointer.
Unfortunately in the view of swift this could potentially be a null pointer and hence becomes imported as an optional pointer type.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Long-term, once we have a separate module map and platform overlay, we should API-note it with the appropriate nullability information.
swiftlang/swift#81407

#else
typealias MutexType = pthread_mutex_t
#endif

private nonisolated(unsafe) var continuations = [pid_t: CheckedContinuation<TerminationStatus, any Error>]()
private nonisolated(unsafe) let mutex = UnsafeMutablePointer<pthread_mutex_t>.allocate(capacity: 1)
private nonisolated(unsafe) let mutex = UnsafeMutablePointer<MutexType>.allocate(capacity: 1)

init() {
pthread_mutex_init(mutex, nil)
Expand All @@ -298,7 +304,7 @@ private final class ChildProcessContinuations: Sendable {
}
}

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

private nonisolated(unsafe) let _waitThreadNoChildrenCondition = {
#if os(FreeBSD) || os(OpenBSD)
let result = UnsafeMutablePointer<pthread_cond_t?>.allocate(capacity: 1)
#else
let result = UnsafeMutablePointer<pthread_cond_t>.allocate(capacity: 1)
#endif
_ = pthread_cond_init(result, nil)
return result
}()

#if !os(FreeBSD) && !os(OpenBSD)
private extension siginfo_t {
var si_status: Int32 {
#if canImport(Glibc)
Expand All @@ -336,11 +347,16 @@ private extension siginfo_t {
#endif
}
}
#endif

private let setup: () = {
// Create the thread. It will run immediately; because it runs in an infinite
// loop, we aren't worried about detaching or joining it.
#if os(FreeBSD) || os(OpenBSD)
var thread: pthread_t?
#else
var thread = pthread_t()
#endif
_ = pthread_create(
&thread,
nil,
Expand Down
6 changes: 3 additions & 3 deletions Sources/Subprocess/Platforms/Subprocess+Unix.swift
Original file line number Diff line number Diff line change
Expand Up @@ -176,10 +176,10 @@ extension Environment {
/// length = `key` + `=` + `value` + `\null`
let totalLength = keyContainer.count + 1 + valueContainer.count + 1
let fullString: UnsafeMutablePointer<CChar> = .allocate(capacity: totalLength)
#if canImport(Darwin)
_ = snprintf(ptr: fullString, totalLength, "%s=%s", rawByteKey, rawByteValue)
#else
#if os(Linux)
_ = _shims_snprintf(fullString, CInt(totalLength), "%s=%s", rawByteKey, rawByteValue)
#else
_ = snprintf(ptr: fullString, totalLength, "%s=%s", rawByteKey, rawByteValue)
#endif
return fullString
}
Expand Down
2 changes: 1 addition & 1 deletion Sources/_SubprocessCShims/process_shims.c
Original file line number Diff line number Diff line change
Expand Up @@ -298,7 +298,7 @@ int _subprocess_spawn(
#endif // TARGET_OS_MAC

// MARK: - Linux (fork/exec + posix_spawn fallback)
#if TARGET_OS_LINUX
#if TARGET_OS_LINUX || TARGET_OS_BSD
#ifndef __GLIBC_PREREQ
#define __GLIBC_PREREQ(maj, min) 0
#endif
Expand Down
2 changes: 1 addition & 1 deletion Tests/SubprocessTests/SubprocessTests+Linux.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
//
//===----------------------------------------------------------------------===//

#if canImport(Glibc) || canImport(Bionic) || canImport(Musl)
#if os(Linux) || os(Android)

#if canImport(Bionic)
import Bionic
Expand Down
41 changes: 23 additions & 18 deletions Tests/SubprocessTests/SubprocessTests+Unix.swift
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ extension SubprocessUnixTests {
extension SubprocessUnixTests {
@Test func testArgumentsArrayLiteral() async throws {
let result = try await Subprocess.run(
.path("/bin/bash"),
.path("/bin/sh"),
arguments: ["-c", "echo Hello World!"],
output: .string
)
Expand All @@ -117,7 +117,7 @@ extension SubprocessUnixTests {

@Test func testArgumentsOverride() async throws {
let result = try await Subprocess.run(
.path("/bin/bash"),
.path("/bin/sh"),
arguments: .init(
executablePathOverride: "apple",
remainingValues: ["-c", "echo $0"]
Expand Down Expand Up @@ -157,7 +157,7 @@ extension SubprocessUnixTests {
extension SubprocessUnixTests {
@Test func testEnvironmentInherit() async throws {
let result = try await Subprocess.run(
.path("/bin/bash"),
.path("/bin/sh"),
arguments: ["-c", "printenv PATH"],
environment: .inherit,
output: .string
Expand All @@ -173,7 +173,7 @@ extension SubprocessUnixTests {

@Test func testEnvironmentInheritOverride() async throws {
let result = try await Subprocess.run(
.path("/bin/bash"),
.path("/bin/sh"),
arguments: ["-c", "printenv HOME"],
environment: .inherit.updating([
"HOME": "/my/new/home"
Expand Down Expand Up @@ -595,7 +595,7 @@ extension SubprocessUnixTests {
contentsOf: URL(filePath: theMysteriousIsland.string)
)
let catResult = try await Subprocess.run(
.path("/bin/bash"),
.path("/bin/sh"),
arguments: ["-c", "cat \(theMysteriousIsland.string) 1>&2"],
error: .data(limit: 2048 * 1024)
)
Expand Down Expand Up @@ -668,11 +668,14 @@ extension SubprocessUnixTests {
var platformOptions = PlatformOptions()
platformOptions.supplementaryGroups = Array(expectedGroups)
let idResult = try await Subprocess.run(
.path("/usr/bin/swift"),
.name("swift"),
arguments: [getgroupsSwift.string],
platformOptions: platformOptions,
output: .string
output: .string,
error: .string,
)
let error = try #require(idResult.standardError)
try #require(error == "")
#expect(idResult.terminationStatus.isSuccess)
let ids = try #require(
idResult.standardOutput
Expand All @@ -696,7 +699,7 @@ extension SubprocessUnixTests {
// Sets the process group ID to 0, which creates a new session
platformOptions.processGroupID = 0
let psResult = try await Subprocess.run(
.path("/bin/bash"),
.path("/bin/sh"),
arguments: ["-c", "ps -o pid,pgid -p $$"],
platformOptions: platformOptions,
output: .string
Expand All @@ -723,22 +726,22 @@ extension SubprocessUnixTests {
// Check the process ID (pid), process group ID (pgid), and
// controlling terminal's process group ID (tpgid)
let psResult = try await Subprocess.run(
.path("/bin/bash"),
.path("/bin/sh"),
arguments: ["-c", "ps -o pid,pgid,tpgid -p $$"],
platformOptions: platformOptions,
output: .string
)
try assertNewSessionCreated(with: psResult)
}

@Test func testTeardownSequence() async throws {
@Test(.requiresBash) func testTeardownSequence() async throws {
let result = try await Subprocess.run(
.path("/bin/bash"),
.name("bash"),
arguments: [
"-c",
"""
set -e
trap 'echo saw SIGQUIT;' SIGQUIT
trap 'echo saw SIGQUIT;' QUIT
trap 'echo saw SIGTERM;' TERM
trap 'echo saw SIGINT; exit 42;' INT
while true; do sleep 1; done
Expand Down Expand Up @@ -777,7 +780,7 @@ extension SubprocessUnixTests {
@Test func testRunDetached() async throws {
let (readFd, writeFd) = try FileDescriptor.pipe()
let pid = try runDetached(
.path("/bin/bash"),
.path("/bin/sh"),
arguments: ["-c", "echo $$"],
output: writeFd
)
Expand Down Expand Up @@ -1046,11 +1049,11 @@ extension FileDescriptor {

// MARK: - Performance Tests
extension SubprocessUnixTests {
@Test func testConcurrentRun() async throws {
@Test(.requiresBash) func testConcurrentRun() async throws {
// Launch as many processes as we can
// Figure out the max open file limit
let limitResult = try await Subprocess.run(
.path("/bin/bash"),
.path("/bin/sh"),
arguments: ["-c", "ulimit -n"],
output: .string
)
Expand All @@ -1075,8 +1078,9 @@ extension SubprocessUnixTests {
let byteCount = 1000
for _ in 0..<maxConcurrent {
group.addTask {
// This invocation specifically requires bash semantics; sh (on FreeBSD at least) does not consistently support -s in this way
let r = try await Subprocess.run(
.path("/bin/bash"),
.name("bash"),
arguments: [
"-sc", #"echo "$1" && echo "$1" >&2"#, "--", String(repeating: "X", count: byteCount),
],
Expand All @@ -1099,13 +1103,14 @@ extension SubprocessUnixTests {
}
}

@Test func testCaptureLongStandardOutputAndError() async throws {
@Test(.requiresBash) func testCaptureLongStandardOutputAndError() async throws {
try await withThrowingTaskGroup(of: Void.self) { group in
var running = 0
for _ in 0..<10 {
group.addTask {
// This invocation specifically requires bash semantics; sh (on FreeBSD at least) does not consistently support -s in this way
let r = try await Subprocess.run(
.path("/bin/bash"),
.name("bash"),
arguments: [
"-sc", #"echo "$1" && echo "$1" >&2"#, "--", String(repeating: "X", count: 100_000),
],
Expand Down
12 changes: 12 additions & 0 deletions Tests/SubprocessTests/TestSupport.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ import Foundation
import FoundationEssentials
#endif

import Testing
import Subprocess

internal func randomString(length: Int, lettersOnly: Bool = false) -> String {
let letters: String
if lettersOnly {
Expand All @@ -42,3 +45,12 @@ internal func directory(_ lhs: String, isSameAs rhs: String) -> Bool {

return canonicalLhs == canonicalRhs
}

extension Trait where Self == ConditionTrait {
public static var requiresBash: Self {
enabled(
if: (try? Executable.name("bash").resolveExecutablePath(in: .inherit)) != nil,
"This test requires bash (install `bash` package on Linux/BSD)"
)
}
}