diff --git a/Package.swift b/Package.swift index 259694a..0badc42 100644 --- a/Package.swift +++ b/Package.swift @@ -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 diff --git a/Sources/Subprocess/Platforms/Subprocess+Linux.swift b/Sources/Subprocess/Platforms/Subprocess+Linux.swift index 125ba46..bf1e5b8 100644 --- a/Sources/Subprocess/Platforms/Subprocess+Linux.swift +++ b/Sources/Subprocess/Platforms/Subprocess+Linux.swift @@ -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? + #else + typealias MutexType = pthread_mutex_t + #endif + private nonisolated(unsafe) var continuations = [pid_t: CheckedContinuation]() - private nonisolated(unsafe) let mutex = UnsafeMutablePointer.allocate(capacity: 1) + private nonisolated(unsafe) let mutex = UnsafeMutablePointer.allocate(capacity: 1) init() { pthread_mutex_init(mutex, nil) @@ -298,7 +304,7 @@ private final class ChildProcessContinuations: Sendable { } } - func withUnsafeUnderlyingLock(_ body: (UnsafeMutablePointer, inout [pid_t: CheckedContinuation]) throws -> R) rethrows -> R { + func withUnsafeUnderlyingLock(_ body: (UnsafeMutablePointer, inout [pid_t: CheckedContinuation]) throws -> R) rethrows -> R { pthread_mutex_lock(mutex) defer { pthread_mutex_unlock(mutex) @@ -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.allocate(capacity: 1) + #else let result = UnsafeMutablePointer.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) @@ -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, diff --git a/Sources/Subprocess/Platforms/Subprocess+Unix.swift b/Sources/Subprocess/Platforms/Subprocess+Unix.swift index 268d32e..bb5da2f 100644 --- a/Sources/Subprocess/Platforms/Subprocess+Unix.swift +++ b/Sources/Subprocess/Platforms/Subprocess+Unix.swift @@ -176,10 +176,10 @@ extension Environment { /// length = `key` + `=` + `value` + `\null` let totalLength = keyContainer.count + 1 + valueContainer.count + 1 let fullString: UnsafeMutablePointer = .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 } diff --git a/Sources/_SubprocessCShims/process_shims.c b/Sources/_SubprocessCShims/process_shims.c index 55a2364..cf7ca66 100644 --- a/Sources/_SubprocessCShims/process_shims.c +++ b/Sources/_SubprocessCShims/process_shims.c @@ -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 diff --git a/Tests/SubprocessTests/SubprocessTests+Linux.swift b/Tests/SubprocessTests/SubprocessTests+Linux.swift index 81d2318..9ad4e29 100644 --- a/Tests/SubprocessTests/SubprocessTests+Linux.swift +++ b/Tests/SubprocessTests/SubprocessTests+Linux.swift @@ -9,7 +9,7 @@ // //===----------------------------------------------------------------------===// -#if canImport(Glibc) || canImport(Bionic) || canImport(Musl) +#if os(Linux) || os(Android) #if canImport(Bionic) import Bionic diff --git a/Tests/SubprocessTests/SubprocessTests+Unix.swift b/Tests/SubprocessTests/SubprocessTests+Unix.swift index 154d46c..a50c4e8 100644 --- a/Tests/SubprocessTests/SubprocessTests+Unix.swift +++ b/Tests/SubprocessTests/SubprocessTests+Unix.swift @@ -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 ) @@ -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"] @@ -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 @@ -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" @@ -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) ) @@ -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 @@ -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 @@ -723,7 +726,7 @@ 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 @@ -731,14 +734,14 @@ extension SubprocessUnixTests { 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 @@ -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 ) @@ -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 ) @@ -1075,8 +1078,9 @@ extension SubprocessUnixTests { let byteCount = 1000 for _ in 0..&2"#, "--", String(repeating: "X", count: byteCount), ], @@ -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), ], diff --git a/Tests/SubprocessTests/TestSupport.swift b/Tests/SubprocessTests/TestSupport.swift index 8939ac2..b8e75a1 100644 --- a/Tests/SubprocessTests/TestSupport.swift +++ b/Tests/SubprocessTests/TestSupport.swift @@ -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 { @@ -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)" + ) + } +}