Skip to content

Commit bcac775

Browse files
Enable FreeBSD CI (#238)
Also fixes FreeBSD/OpenBSD build issues due to different pthread definitions, and an ordering issue where setgroups, setuid, and setgid were called in a different order from the macOS implementations (setgroups needs root so must be called first).
1 parent 1ed52bd commit bcac775

13 files changed

Lines changed: 113 additions & 77 deletions

File tree

.github/workflows/pull_request.yml

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ on:
1010
jobs:
1111
tests:
1212
name: Test
13-
uses: swiftlang/github-workflows/.github/workflows/swift_package_test.yml@0.0.6
13+
uses: swiftlang/github-workflows/.github/workflows/swift_package_test.yml@0.0.10
1414
with:
1515
linux_os_versions: '["amazonlinux2", "bookworm", "noble", "jammy", "rhel-ubi9"]'
1616
linux_swift_versions: '["6.2", "nightly-main"]'
@@ -36,6 +36,8 @@ jobs:
3636
yum install -y procps
3737
fi
3838
linux_build_command: 'swift test && swift test -c release && swift test --disable-default-traits'
39+
enable_freebsd_checks: true
40+
freebsd_build_command: 'swift test && swift test -c release && swift test --disable-default-traits'
3941
windows_swift_versions: '["6.2", "nightly-main"]'
4042
windows_build_command: |
4143
Invoke-Program swift test
@@ -57,7 +59,7 @@ jobs:
5759
5860
soundness:
5961
name: Soundness
60-
uses: swiftlang/github-workflows/.github/workflows/soundness.yml@0.0.6
62+
uses: swiftlang/github-workflows/.github/workflows/soundness.yml@0.0.10
6163
with:
6264
license_header_check_project_name: "Swift.org"
6365
docs_check_enabled: false

Sources/Subprocess/Thread.swift

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,12 @@ import Synchronization
3333
private typealias MutexType = CRITICAL_SECTION
3434
private typealias ConditionType = CONDITION_VARIABLE
3535
private typealias ThreadType = HANDLE
36+
#elseif os(FreeBSD) || os(OpenBSD)
37+
// On FreeBSD/OpenBSD, pthread types are pointer types (e.g. struct pthread_mutex *),
38+
// and pthread functions expect nullable pointers to them.
39+
private typealias MutexType = pthread_mutex_t?
40+
private typealias ConditionType = pthread_cond_t?
41+
private typealias ThreadType = pthread_t
3642
#else
3743
private typealias MutexType = pthread_mutex_t
3844
private typealias ConditionType = pthread_cond_t
@@ -320,7 +326,7 @@ internal func pthread_create(
320326
(Unmanaged<AnyObject>.fromOpaque(context!).takeRetainedValue() as! Context).body()
321327
return context
322328
}
323-
#if canImport(Glibc) || canImport(Bionic)
329+
#if (canImport(Glibc) || canImport(Bionic)) && !os(FreeBSD) && !os(OpenBSD)
324330
var thread = pthread_t()
325331
#else
326332
var thread: pthread_t?
@@ -334,7 +340,7 @@ internal func pthread_create(
334340
if rc != 0 {
335341
throw Errno(rawValue: rc)
336342
}
337-
#if canImport(Glibc) || canImport(Bionic)
343+
#if (canImport(Glibc) || canImport(Bionic)) && !os(FreeBSD) && !os(OpenBSD)
338344
return thread
339345
#else
340346
return thread!

Sources/_SubprocessCShims/include/process_shims.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ extern "C" {
4141
#endif
4242

4343
int _subprocess_pthread_create(
44-
#if TARGET_OS_MAC
44+
#if TARGET_OS_MAC || defined(__FreeBSD__) || defined(__OpenBSD__)
4545
pthread_t _Nullable * _Nonnull ptr,
4646
#else
4747
pthread_t * _Nonnull ptr,

Sources/_SubprocessCShims/process_shims.c

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -615,23 +615,23 @@ int _subprocess_fork_exec(
615615
}
616616
}
617617

618-
if (uid != NULL) {
619-
if (setuid(*uid) != 0) {
618+
if (number_of_sgroups > 0 && sgroups != NULL) {
619+
// POSIX doesn't define setgroups (only getgroups) and therefore makes no guarantee of async-signal-safety,
620+
// but we'll assume in practice it should be async-signal-safe on any reasonable platform based on the fact
621+
// that getgroups is async-signal-safe.
622+
if (setgroups(number_of_sgroups, sgroups) != 0) {
620623
write_error_and_exit;
621624
}
622625
}
623626

624-
if (gid != NULL) {
625-
if (setgid(*gid) != 0) {
627+
if (uid != NULL) {
628+
if (setuid(*uid) != 0) {
626629
write_error_and_exit;
627630
}
628631
}
629632

630-
if (number_of_sgroups > 0 && sgroups != NULL) {
631-
// POSIX doesn't define setgroups (only getgroups) and therefore makes no guarantee of async-signal-safety,
632-
// but we'll assume in practice it should be async-signal-safe on any reasonable platform based on the fact
633-
// that getgroups is async-signal-safe.
634-
if (setgroups(number_of_sgroups, sgroups) != 0) {
633+
if (gid != NULL) {
634+
if (setgid(*gid) != 0) {
635635
write_error_and_exit;
636636
}
637637
}

Tests/SubprocessTests/AsyncIOTests.swift

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,11 @@ import _SubprocessCShims
3535
@testable import Subprocess
3636

3737
@Suite("Subprocess.AsyncIO Unit Tests", .serialized)
38-
struct SubprocessAsyncIOTests {}
38+
struct SubprocessAsyncIOTests {
39+
init() {
40+
_ = globallyIgnoredSIGPIPE
41+
}
42+
}
3943

4044
// MARK: - Basic Functionality Tests
4145
extension SubprocessAsyncIOTests {
@@ -228,7 +232,7 @@ extension SubprocessAsyncIOTests {
228232

229233
// MARK: - Utils
230234
extension SubprocessAsyncIOTests {
231-
final class TestBed {
235+
final class TestBed: Sendable {
232236
let ioChannel: Subprocess.IOChannel
233237

234238
init(ioChannel: consuming Subprocess.IOChannel) {
@@ -255,18 +259,23 @@ extension SubprocessAsyncIOTests {
255259
let readIO = AsyncIO.shared
256260
let writeIO = AsyncIO()
257261

262+
// Create TestBeds in outer scope so both pipe endpoints
263+
// stay alive until both tasks complete
264+
let readTestBed = try TestBed(ioChannel: _require(readBox.take()))
265+
let writeTestBed = try TestBed(ioChannel: _require(writeBox.take()))
266+
258267
group.addTask {
259-
var readIOContainer: IOChannel? = readBox.take()
260-
let readTestBed = try TestBed(ioChannel: _require(readIOContainer.take()))
261268
try await reader(readIO, readTestBed)
262269
}
263270
group.addTask {
264-
var writeIOContainer: IOChannel? = writeBox.take()
265-
let writeTestBed = try TestBed(ioChannel: _require(writeIOContainer.take()))
266271
try await writer(writeIO, writeTestBed)
267272
}
268273

269274
try await group.waitForAll()
275+
// Keep both pipe endpoints alive until both tasks complete,
276+
// preventing the DispatchIO cleanup handler from closing
277+
// a pipe fd while the other task is still using it.
278+
withExtendedLifetime((readTestBed, writeTestBed)) {}
270279
// Teardown
271280
// readIO shutdown is done via `atexit`.
272281
writeIO.shutdown()

Tests/SubprocessTests/DarwinTests.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,10 @@ import SystemPackage
2626
// MARK: PlatformOptions Tests
2727
@Suite(.serialized)
2828
struct SubprocessDarwinTests {
29+
init() {
30+
_ = globallyIgnoredSIGPIPE
31+
}
32+
2933
@Test func testSubprocessPlatformOptionsProcessConfiguratorUpdateSpawnAttr() async throws {
3034
var platformOptions = PlatformOptions()
3135
platformOptions.preSpawnProcessConfigurator = { spawnAttr, _ in

Tests/SubprocessTests/IntegrationTests.swift

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,11 @@ import _SubprocessCShims
3535
@testable import Subprocess
3636

3737
@Suite("Subprocess Integration (End to End) Tests", .serialized)
38-
struct SubprocessIntegrationTests {}
38+
struct SubprocessIntegrationTests {
39+
init() {
40+
_ = globallyIgnoredSIGPIPE
41+
}
42+
}
3943

4044
// MARK: - Executable Tests
4145
extension SubprocessIntegrationTests {

Tests/SubprocessTests/LinuxTests.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,10 @@ import _SubprocessCShims
3535
// MARK: PlatformOption Tests
3636
@Suite(.serialized)
3737
struct SubprocessLinuxTests {
38+
init() {
39+
_ = globallyIgnoredSIGPIPE
40+
}
41+
3842
@Test func testSuspendResumeProcess() async throws {
3943
func blockAndWaitForStatus(
4044
pid: pid_t,

Tests/SubprocessTests/ProcessMonitoringTests.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ import _SubprocessCShims
3838
struct SubprocessProcessMonitoringTests {
3939

4040
init() {
41+
_ = globallyIgnoredSIGPIPE
4142
#if os(Linux) || os(Android)
4243
_setupMonitorSignalHandler()
4344
#endif

Tests/SubprocessTests/TestSupport.swift

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,29 @@ import FoundationEssentials
2020
import Testing
2121
import Subprocess
2222

23+
#if canImport(Darwin)
24+
import Darwin
25+
#elseif canImport(Glibc)
26+
import Glibc
27+
#elseif canImport(Bionic)
28+
import Bionic
29+
#elseif canImport(Musl)
30+
import Musl
31+
#endif
32+
33+
/// We receive a `SIGPIPE` if we write to a closed pipe, which crashes the process.
34+
///
35+
/// Some platforms have API that can be used to set state bits on a file descriptor
36+
/// that turn a `SIGPIPE` into an `EPIPE` instead, but this is not consistently available.
37+
///
38+
/// Instead, globally ignore `SIGPIPE` in tests to prevent us from crashing in this scenario.
39+
internal let globallyIgnoredSIGPIPE: Bool = {
40+
#if canImport(Darwin) || canImport(Glibc) || canImport(Bionic) || canImport(Musl)
41+
_ = signal(SIGPIPE, SIG_IGN)
42+
#endif
43+
return true
44+
}()
45+
2346
#if canImport(System)
2447
import System
2548
#else

0 commit comments

Comments
 (0)