From ded1894210b66c27f08aa6a76a1de0648e22aeec Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Mon, 27 Oct 2025 11:46:51 -0400 Subject: [PATCH 01/17] [WIP] Enable exit tests on Android API level 28 and newer. --- Package.swift | 4 ++-- .../Attachments/Attachment+URL.swift | 17 +++++++++++++---- Sources/Testing/ExitTests/ExitStatus.swift | 15 ++++++++++++--- .../ExitTests/ExitTest.CapturedValue.swift | 4 +++- .../Testing/ExitTests/ExitTest.Condition.swift | 16 ++++++++++++---- Sources/Testing/ExitTests/ExitTest.Result.swift | 4 +++- Sources/Testing/ExitTests/ExitTest.swift | 8 ++++++-- Sources/Testing/ExitTests/SpawnProcess.swift | 11 ++++++++--- Sources/Testing/ExitTests/WaitFor.swift | 12 ++++++++---- .../Expectations/Expectation+Macro.swift | 8 ++++++-- cmake/modules/shared/CompilerSettings.cmake | 4 ++-- 11 files changed, 75 insertions(+), 28 deletions(-) diff --git a/Package.swift b/Package.swift index 2788502c3..c64c77ca3 100644 --- a/Package.swift +++ b/Package.swift @@ -377,7 +377,7 @@ extension Array where Element == PackageDescription.SwiftSetting { .define("SWT_TARGET_OS_APPLE", .whenApple()), - .define("SWT_NO_EXIT_TESTS", .whenEmbedded(or: .when(platforms: [.iOS, .watchOS, .tvOS, .visionOS, .wasi, .android]))), + .define("SWT_NO_EXIT_TESTS", .whenEmbedded(or: .when(platforms: [.iOS, .watchOS, .tvOS, .visionOS, .wasi]))), .define("SWT_NO_PROCESS_SPAWNING", .whenEmbedded(or: .when(platforms: [.iOS, .watchOS, .tvOS, .visionOS, .wasi, .android]))), .define("SWT_NO_SNAPSHOT_TYPES", .whenEmbedded(or: .whenApple(false))), .define("SWT_NO_DYNAMIC_LINKING", .whenEmbedded(or: .when(platforms: [.wasi]))), @@ -437,7 +437,7 @@ extension Array where Element == PackageDescription.CXXSetting { result += [ .define("SWT_NO_EXIT_TESTS", .whenEmbedded(or: .when(platforms: [.iOS, .watchOS, .tvOS, .visionOS, .wasi, .android]))), - .define("SWT_NO_PROCESS_SPAWNING", .whenEmbedded(or: .when(platforms: [.iOS, .watchOS, .tvOS, .visionOS, .wasi, .android]))), + .define("SWT_NO_PROCESS_SPAWNING", .whenEmbedded(or: .when(platforms: [.iOS, .watchOS, .tvOS, .visionOS, .wasi]))), .define("SWT_NO_SNAPSHOT_TYPES", .whenEmbedded(or: .whenApple(false))), .define("SWT_NO_DYNAMIC_LINKING", .whenEmbedded(or: .when(platforms: [.wasi]))), .define("SWT_NO_PIPES", .whenEmbedded(or: .when(platforms: [.wasi]))), diff --git a/Sources/Overlays/_Testing_Foundation/Attachments/Attachment+URL.swift b/Sources/Overlays/_Testing_Foundation/Attachments/Attachment+URL.swift index 3ca05b8d1..a7fec71ea 100644 --- a/Sources/Overlays/_Testing_Foundation/Attachments/Attachment+URL.swift +++ b/Sources/Overlays/_Testing_Foundation/Attachments/Attachment+URL.swift @@ -160,6 +160,13 @@ private let _archiverPath: String? = { /// an archive (currently of `.zip` format, although this is subject to change.) private func _compressContentsOfDirectory(at directoryURL: URL) async throws -> Data { #if !SWT_NO_PROCESS_SPAWNING +#if os(Android) + guard #available(Android 28, *) else { + // API level 28 corresponds to Android 9 Pie. + throw CocoaError(.featureUnsupported, userInfo: [NSLocalizedDescriptionKey: "Attaching directories to tests requires Android 9 (API level 28) or newer."]) + } +#endif + let temporaryName = "\(UUID().uuidString).zip" let temporaryURL = FileManager.default.temporaryDirectory.appendingPathComponent(temporaryName) defer { @@ -180,12 +187,15 @@ private func _compressContentsOfDirectory(at directoryURL: URL) async throws -> // OpenBSD's tar(1) does not support writing PKZIP archives, and /usr/bin/zip // tool is an optional install, so we check if it's present before trying to // execute it. -#if os(Linux) || os(OpenBSD) + // + // TODO: figure out whether tar or zip is available on Android and where it's stored +#if os(Linux) || os(OpenBSD) || os(Android) let archiverPath = "/bin/sh" -#if os(Linux) +#if os(Linux) || os(Android) let trueArchiverPath = "/usr/bin/zip" #else let trueArchiverPath = "/usr/local/bin/zip" +#endif var isDirectory = false if !FileManager.default.fileExists(atPath: trueArchiverPath, isDirectory: &isDirectory) || isDirectory { throw CocoaError(.fileNoSuchFile, userInfo: [ @@ -193,7 +203,6 @@ private func _compressContentsOfDirectory(at directoryURL: URL) async throws -> NSFilePathErrorKey: trueArchiverPath ]) } -#endif #elseif SWT_TARGET_OS_APPLE || os(FreeBSD) let archiverPath = "/usr/bin/tar" #elseif os(Windows) @@ -211,7 +220,7 @@ private func _compressContentsOfDirectory(at directoryURL: URL) async throws -> let sourcePath = directoryURL.path let destinationPath = temporaryURL.path let arguments = { -#if os(Linux) || os(OpenBSD) +#if os(Linux) || os(OpenBSD) || os(Android) // The zip command constructs relative paths from the current working // directory rather than from command-line arguments. ["-c", #"cd "$0" && "$1" "$2" --recurse-paths ."#, sourcePath, trueArchiverPath, destinationPath] diff --git a/Sources/Testing/ExitTests/ExitStatus.swift b/Sources/Testing/ExitTests/ExitStatus.swift index 21fa2335e..031348681 100644 --- a/Sources/Testing/ExitTests/ExitStatus.swift +++ b/Sources/Testing/ExitTests/ExitStatus.swift @@ -23,7 +23,10 @@ private import _TestingInternals /// @Available(Swift, introduced: 6.2) /// @Available(Xcode, introduced: 26.0) /// } -#if SWT_NO_PROCESS_SPAWNING +#if !SWT_NO_PROCESS_SPAWNING +@available(Android 28, *) +#else +@_unavailableInEmbedded @available(*, unavailable, message: "Exit tests are not available on this platform.") #endif public enum ExitStatus: Sendable { @@ -90,7 +93,10 @@ public enum ExitStatus: Sendable { // MARK: - Equatable -#if SWT_NO_PROCESS_SPAWNING +#if !SWT_NO_PROCESS_SPAWNING +@available(Android 28, *) +#else +@_unavailableInEmbedded @available(*, unavailable, message: "Exit tests are not available on this platform.") #endif extension ExitStatus: Equatable {} @@ -109,7 +115,10 @@ private let _sigabbrev_np = symbol(named: "sigabbrev_np").map { } #endif -#if SWT_NO_PROCESS_SPAWNING +#if !SWT_NO_PROCESS_SPAWNING +@available(Android 28, *) +#else +@_unavailableInEmbedded @available(*, unavailable, message: "Exit tests are not available on this platform.") #endif extension ExitStatus: CustomStringConvertible { diff --git a/Sources/Testing/ExitTests/ExitTest.CapturedValue.swift b/Sources/Testing/ExitTests/ExitTest.CapturedValue.swift index 556fc0cf6..28ce5d4d5 100644 --- a/Sources/Testing/ExitTests/ExitTest.CapturedValue.swift +++ b/Sources/Testing/ExitTests/ExitTest.CapturedValue.swift @@ -11,7 +11,9 @@ private import _TestingInternals @_spi(ForToolsIntegrationOnly) -#if SWT_NO_EXIT_TESTS +#if !SWT_NO_EXIT_TESTS +@available(Android 28, *) +#else @_unavailableInEmbedded @available(*, unavailable, message: "Exit tests are not available on this platform.") #endif diff --git a/Sources/Testing/ExitTests/ExitTest.Condition.swift b/Sources/Testing/ExitTests/ExitTest.Condition.swift index edd94193b..1d1494fbc 100644 --- a/Sources/Testing/ExitTests/ExitTest.Condition.swift +++ b/Sources/Testing/ExitTests/ExitTest.Condition.swift @@ -10,7 +10,9 @@ private import _TestingInternals -#if SWT_NO_EXIT_TESTS +#if !SWT_NO_EXIT_TESTS +@available(Android 28, *) +#else @_unavailableInEmbedded @available(*, unavailable, message: "Exit tests are not available on this platform.") #endif @@ -58,7 +60,9 @@ extension ExitTest { // MARK: - -#if SWT_NO_EXIT_TESTS +#if !SWT_NO_EXIT_TESTS +@available(Android 28, *) +#else @_unavailableInEmbedded @available(*, unavailable, message: "Exit tests are not available on this platform.") #endif @@ -178,7 +182,9 @@ extension ExitTest.Condition { // MARK: - CustomStringConvertible -#if SWT_NO_EXIT_TESTS +#if !SWT_NO_EXIT_TESTS +@available(Android 28, *) +#else @_unavailableInEmbedded @available(*, unavailable, message: "Exit tests are not available on this platform.") #endif @@ -201,7 +207,9 @@ extension ExitTest.Condition: CustomStringConvertible { // MARK: - Comparison -#if SWT_NO_EXIT_TESTS +#if !SWT_NO_EXIT_TESTS +@available(Android 28, *) +#else @_unavailableInEmbedded @available(*, unavailable, message: "Exit tests are not available on this platform.") #endif diff --git a/Sources/Testing/ExitTests/ExitTest.Result.swift b/Sources/Testing/ExitTests/ExitTest.Result.swift index 53d816c85..3c72e43ed 100644 --- a/Sources/Testing/ExitTests/ExitTest.Result.swift +++ b/Sources/Testing/ExitTests/ExitTest.Result.swift @@ -8,7 +8,9 @@ // See https://swift.org/CONTRIBUTORS.txt for Swift project authors // -#if SWT_NO_EXIT_TESTS +#if !SWT_NO_EXIT_TESTS +@available(Android 28, *) +#else @_unavailableInEmbedded @available(*, unavailable, message: "Exit tests are not available on this platform.") #endif diff --git a/Sources/Testing/ExitTests/ExitTest.swift b/Sources/Testing/ExitTests/ExitTest.swift index 56096906f..15bdfc2d0 100644 --- a/Sources/Testing/ExitTests/ExitTest.swift +++ b/Sources/Testing/ExitTests/ExitTest.swift @@ -34,7 +34,9 @@ private import _TestingInternals /// @Available(Swift, introduced: 6.2) /// @Available(Xcode, introduced: 26.0) /// } -#if SWT_NO_EXIT_TESTS +#if !SWT_NO_EXIT_TESTS +@available(Android 28, *) +#else @_unavailableInEmbedded @available(*, unavailable, message: "Exit tests are not available on this platform.") #endif @@ -223,6 +225,8 @@ extension ExitTest { // as I can tell, special-case RLIMIT_CORE=1. var rl = rlimit(rlim_cur: 0, rlim_max: 0) _ = setrlimit(RLIMIT_CORE, &rl) +#elseif os(Android) + // TODO: "tombstoned_intercept"? #elseif os(Windows) // On Windows, similarly disable Windows Error Reporting and the Windows // Error Reporting UI. Note we expect to be the first component to call @@ -698,7 +702,7 @@ extension ExitTest { /// back to a (new) file handle with `_makeFileHandle()`, or `nil` if the /// file handle could not be converted to a string. private static func _makeEnvironmentVariable(for fileHandle: borrowing FileHandle) -> String? { -#if SWT_TARGET_OS_APPLE || os(Linux) || os(FreeBSD) || os(OpenBSD) +#if SWT_TARGET_OS_APPLE || os(Linux) || os(FreeBSD) || os(OpenBSD) || os(Android) return fileHandle.withUnsafePOSIXFileDescriptor { fd in fd.map(String.init(describing:)) } diff --git a/Sources/Testing/ExitTests/SpawnProcess.swift b/Sources/Testing/ExitTests/SpawnProcess.swift index 6114566f1..4fb6e50e2 100644 --- a/Sources/Testing/ExitTests/SpawnProcess.swift +++ b/Sources/Testing/ExitTests/SpawnProcess.swift @@ -17,7 +17,7 @@ internal import _TestingInternals /// A platform-specific value identifying a process running on the current /// system. -#if SWT_TARGET_OS_APPLE || os(Linux) || os(FreeBSD) || os(OpenBSD) +#if SWT_TARGET_OS_APPLE || os(Linux) || os(FreeBSD) || os(OpenBSD) || os(Android) typealias ProcessID = pid_t #elseif os(Windows) typealias ProcessID = HANDLE @@ -62,6 +62,7 @@ private let _posix_spawn_file_actions_addclosefrom_np = symbol(named: "posix_spa /// resources. /// /// - Throws: Any error that prevented the process from spawning. +@available(Android 28, *) func spawnExecutable( atPath executablePath: String, arguments: [String], @@ -75,11 +76,11 @@ func spawnExecutable( // use, so use this typealias to paper over the differences. #if SWT_TARGET_OS_APPLE || os(FreeBSD) || os(OpenBSD) typealias P = T? -#elseif os(Linux) +#elseif os(Linux) || os(Android) typealias P = T #endif -#if SWT_TARGET_OS_APPLE || os(Linux) || os(FreeBSD) || os(OpenBSD) +#if SWT_TARGET_OS_APPLE || os(Linux) || os(FreeBSD) || os(OpenBSD) || os(Android) return try withUnsafeTemporaryAllocation(of: P.self, capacity: 1) { fileActions in let fileActions = fileActions.baseAddress! let fileActionsInitialized = posix_spawn_file_actions_init(fileActions) @@ -194,6 +195,9 @@ func spawnExecutable( // spawned child process if we control its execution. var environment = environment environment["SWT_CLOSEFROM"] = String(describing: highestFD + 1) +#elseif os(Android) + // Android does not have posix_spawn_file_actions_addclosefrom_np() nor + // closefrom(2), so we don't attempt this operation there. #else #warning("Platform-specific implementation missing: cannot close unused file descriptors") #endif @@ -463,6 +467,7 @@ private func _escapeCommandLine(_ arguments: [String]) -> String { /// This function is a convenience that spawns the given process and waits for /// it to terminate. It is primarily for use by other targets in this package /// such as its cross-import overlays. +@available(Android 28, *) package func spawnExecutableAtPathAndWait( _ executablePath: String, arguments: [String] = [], diff --git a/Sources/Testing/ExitTests/WaitFor.swift b/Sources/Testing/ExitTests/WaitFor.swift index f0326ff3c..dc6f74ee9 100644 --- a/Sources/Testing/ExitTests/WaitFor.swift +++ b/Sources/Testing/ExitTests/WaitFor.swift @@ -11,7 +11,7 @@ #if !SWT_NO_PROCESS_SPAWNING internal import _TestingInternals -#if SWT_TARGET_OS_APPLE || os(Linux) || os(FreeBSD) || os(OpenBSD) +#if SWT_TARGET_OS_APPLE || os(Linux) || os(FreeBSD) || os(OpenBSD) || os(Android) /// Block the calling thread, wait for the target process to exit, and return /// a value describing the conditions under which it exited. /// @@ -78,7 +78,7 @@ func wait(for pid: consuming pid_t) async throws -> ExitStatus { return try _blockAndWait(for: pid) } -#elseif SWT_TARGET_OS_APPLE || os(Linux) || os(FreeBSD) || os(OpenBSD) +#elseif SWT_TARGET_OS_APPLE || os(Linux) || os(FreeBSD) || os(OpenBSD) || os(Android) /// A mapping of awaited child PIDs to their corresponding Swift continuations. private nonisolated(unsafe) let _childProcessContinuations = { let result = ManagedBuffer<[pid_t: CheckedContinuation], pthread_mutex_t>.create( @@ -189,8 +189,9 @@ private let _createWaitThread: Void = { { _ in // Set the thread name to help with diagnostics. Note that different // platforms support different thread name lengths. See MAXTHREADNAMESIZE - // on Darwin, TASK_COMM_LEN on Linux, MAXCOMLEN on FreeBSD, and _MAXCOMLEN - // on OpenBSD. We try to maximize legibility in the available space. + // on Darwin, TASK_COMM_LEN on Linux, MAXCOMLEN on FreeBSD, _MAXCOMLEN on + // OpenBSD, and MAX_TASK_COMM_LEN on Android. We try to maximize + // legibility in the available space. #if SWT_TARGET_OS_APPLE _ = pthread_setname_np("Swift Testing exit test monitor") #elseif os(Linux) @@ -201,6 +202,8 @@ private let _createWaitThread: Void = { pthread_set_name_np(pthread_self(), "SWT ex test monitor") #elseif os(OpenBSD) pthread_set_name_np(pthread_self(), "SWT exit test monitor") +#elseif os(Android) + _ = pthread_setname_np(pthread_self(), "SWT ExT monitor") #else #warning("Platform-specific implementation missing: thread naming unavailable") #endif @@ -233,6 +236,7 @@ private let _createWaitThread: Void = { /// /// On Apple platforms, the libdispatch-based implementation above is more /// efficient because it does not need to permanently reserve a thread. +@available(Android 28, *) func wait(for pid: consuming pid_t) async throws -> ExitStatus { let pid = consume pid diff --git a/Sources/Testing/Expectations/Expectation+Macro.swift b/Sources/Testing/Expectations/Expectation+Macro.swift index ea007f667..281f8259d 100644 --- a/Sources/Testing/Expectations/Expectation+Macro.swift +++ b/Sources/Testing/Expectations/Expectation+Macro.swift @@ -876,7 +876,9 @@ public macro require( /// } @freestanding(expression) @discardableResult -#if SWT_NO_EXIT_TESTS +#if !SWT_NO_EXIT_TESTS +@available(Android 28, *) +#else @_unavailableInEmbedded @available(*, unavailable, message: "Exit tests are not available on this platform.") #endif @@ -924,7 +926,9 @@ public macro expect( /// } @freestanding(expression) @discardableResult -#if SWT_NO_EXIT_TESTS +#if !SWT_NO_EXIT_TESTS +@available(Android 28, *) +#else @_unavailableInEmbedded @available(*, unavailable, message: "Exit tests are not available on this platform.") #endif diff --git a/cmake/modules/shared/CompilerSettings.cmake b/cmake/modules/shared/CompilerSettings.cmake index b3c0fe3aa..cd961f6de 100644 --- a/cmake/modules/shared/CompilerSettings.cmake +++ b/cmake/modules/shared/CompilerSettings.cmake @@ -26,11 +26,11 @@ add_compile_options( if(APPLE) add_compile_definitions("SWT_TARGET_OS_APPLE") endif() -set(SWT_NO_EXIT_TESTS_LIST "iOS" "watchOS" "tvOS" "visionOS" "WASI" "Android") +set(SWT_NO_EXIT_TESTS_LIST "iOS" "watchOS" "tvOS" "visionOS" "WASI") if(CMAKE_SYSTEM_NAME IN_LIST SWT_NO_EXIT_TESTS_LIST) add_compile_definitions("SWT_NO_EXIT_TESTS") endif() -set(SWT_NO_PROCESS_SPAWNING_LIST "iOS" "watchOS" "tvOS" "visionOS" "WASI" "Android") +set(SWT_NO_PROCESS_SPAWNING_LIST "iOS" "watchOS" "tvOS" "visionOS" "WASI") if(CMAKE_SYSTEM_NAME IN_LIST SWT_NO_PROCESS_SPAWNING_LIST) add_compile_definitions("SWT_NO_PROCESS_SPAWNING") endif() From 670997c4ebe82a26ff43b66d3c00785da64ec3c9 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Mon, 27 Oct 2025 14:38:07 -0400 Subject: [PATCH 02/17] Fix typos --- Package.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Package.swift b/Package.swift index c64c77ca3..96d80c388 100644 --- a/Package.swift +++ b/Package.swift @@ -378,7 +378,7 @@ extension Array where Element == PackageDescription.SwiftSetting { .define("SWT_TARGET_OS_APPLE", .whenApple()), .define("SWT_NO_EXIT_TESTS", .whenEmbedded(or: .when(platforms: [.iOS, .watchOS, .tvOS, .visionOS, .wasi]))), - .define("SWT_NO_PROCESS_SPAWNING", .whenEmbedded(or: .when(platforms: [.iOS, .watchOS, .tvOS, .visionOS, .wasi, .android]))), + .define("SWT_NO_PROCESS_SPAWNING", .whenEmbedded(or: .when(platforms: [.iOS, .watchOS, .tvOS, .visionOS, .wasi]))), .define("SWT_NO_SNAPSHOT_TYPES", .whenEmbedded(or: .whenApple(false))), .define("SWT_NO_DYNAMIC_LINKING", .whenEmbedded(or: .when(platforms: [.wasi]))), .define("SWT_NO_PIPES", .whenEmbedded(or: .when(platforms: [.wasi]))), @@ -436,7 +436,7 @@ extension Array where Element == PackageDescription.CXXSetting { var result = Self() result += [ - .define("SWT_NO_EXIT_TESTS", .whenEmbedded(or: .when(platforms: [.iOS, .watchOS, .tvOS, .visionOS, .wasi, .android]))), + .define("SWT_NO_EXIT_TESTS", .whenEmbedded(or: .when(platforms: [.iOS, .watchOS, .tvOS, .visionOS, .wasi]))), .define("SWT_NO_PROCESS_SPAWNING", .whenEmbedded(or: .when(platforms: [.iOS, .watchOS, .tvOS, .visionOS, .wasi]))), .define("SWT_NO_SNAPSHOT_TYPES", .whenEmbedded(or: .whenApple(false))), .define("SWT_NO_DYNAMIC_LINKING", .whenEmbedded(or: .when(platforms: [.wasi]))), From 7e4bb7fe45f40c66cf4ca263ade8fa4acfdd905c Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Mon, 27 Oct 2025 14:39:52 -0400 Subject: [PATCH 03/17] Fix errors --- Sources/Testing/ExitTests/SpawnProcess.swift | 4 ++-- Sources/_TestingInternals/include/Stubs.h | 2 -- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/Sources/Testing/ExitTests/SpawnProcess.swift b/Sources/Testing/ExitTests/SpawnProcess.swift index 4fb6e50e2..f38039896 100644 --- a/Sources/Testing/ExitTests/SpawnProcess.swift +++ b/Sources/Testing/ExitTests/SpawnProcess.swift @@ -74,9 +74,9 @@ func spawnExecutable( ) throws -> ProcessID { // Darwin and Linux differ in their optionality for the posix_spawn types we // use, so use this typealias to paper over the differences. -#if SWT_TARGET_OS_APPLE || os(FreeBSD) || os(OpenBSD) +#if SWT_TARGET_OS_APPLE || os(FreeBSD) || os(OpenBSD) || os(Android) typealias P = T? -#elseif os(Linux) || os(Android) +#elseif os(Linux) typealias P = T #endif diff --git a/Sources/_TestingInternals/include/Stubs.h b/Sources/_TestingInternals/include/Stubs.h index 636ea9aff..1e743f2f2 100644 --- a/Sources/_TestingInternals/include/Stubs.h +++ b/Sources/_TestingInternals/include/Stubs.h @@ -126,7 +126,6 @@ static char *_Nullable *_Null_unspecified swt_environ(void) { } #endif -#if !defined(__ANDROID__) #if __has_include() && defined(si_pid) /// Get the value of the `si_pid` field of a `siginfo_t` structure. /// @@ -150,7 +149,6 @@ static int swt_siginfo_t_si_status(const siginfo_t *siginfo) { return siginfo->si_status; } #endif -#endif /// Get the value of `EEXIST`. /// From 7e54064d67a1a7cbf4c712ffc15768c0ca4bac27 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Mon, 27 Oct 2025 14:43:20 -0400 Subject: [PATCH 04/17] Differing optionality --- Sources/Testing/ExitTests/SpawnProcess.swift | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/Sources/Testing/ExitTests/SpawnProcess.swift b/Sources/Testing/ExitTests/SpawnProcess.swift index f38039896..6d970967a 100644 --- a/Sources/Testing/ExitTests/SpawnProcess.swift +++ b/Sources/Testing/ExitTests/SpawnProcess.swift @@ -74,10 +74,15 @@ func spawnExecutable( ) throws -> ProcessID { // Darwin and Linux differ in their optionality for the posix_spawn types we // use, so use this typealias to paper over the differences. -#if SWT_TARGET_OS_APPLE || os(FreeBSD) || os(OpenBSD) || os(Android) - typealias P = T? +#if SWT_TARGET_OS_APPLE || os(FreeBSD) || os(OpenBSD) + typealias posix_spawn_file_actions_p = posix_spawn_file_actions_t? + typealias posix_spawnattr_p = posix_spawnattr_t? #elseif os(Linux) - typealias P = T + typealias posix_spawn_file_actions_p = posix_spawn_file_actions_t + typealias posix_spawnattr_p = posix_spawnattr_t +#elseif os(Android) + typealias posix_spawn_file_actions_p = posix_spawn_file_actions_t? + typealias posix_spawnattr_p = posix_spawnattr_t #endif #if SWT_TARGET_OS_APPLE || os(Linux) || os(FreeBSD) || os(OpenBSD) || os(Android) From d3ebaa0a10ad7d89a217db6d4019905a1fa48c90 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Mon, 27 Oct 2025 14:44:40 -0400 Subject: [PATCH 05/17] Seriously Jonathan --- Sources/Testing/ExitTests/SpawnProcess.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/Testing/ExitTests/SpawnProcess.swift b/Sources/Testing/ExitTests/SpawnProcess.swift index 6d970967a..8c05b2393 100644 --- a/Sources/Testing/ExitTests/SpawnProcess.swift +++ b/Sources/Testing/ExitTests/SpawnProcess.swift @@ -86,7 +86,7 @@ func spawnExecutable( #endif #if SWT_TARGET_OS_APPLE || os(Linux) || os(FreeBSD) || os(OpenBSD) || os(Android) - return try withUnsafeTemporaryAllocation(of: P.self, capacity: 1) { fileActions in + return try withUnsafeTemporaryAllocation(of: posix_spawn_file_actions_p.self, capacity: 1) { fileActions in let fileActions = fileActions.baseAddress! let fileActionsInitialized = posix_spawn_file_actions_init(fileActions) guard 0 == fileActionsInitialized else { @@ -96,7 +96,7 @@ func spawnExecutable( _ = posix_spawn_file_actions_destroy(fileActions) } - return try withUnsafeTemporaryAllocation(of: P.self, capacity: 1) { attrs in + return try withUnsafeTemporaryAllocation(of: posix_spawnattr_p.self, capacity: 1) { attrs in let attrs = attrs.baseAddress! let attrsInitialized = posix_spawnattr_init(attrs) guard 0 == attrsInitialized else { From da399c4a17167d7bc2e0ffec6cac83f016bd094c Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Mon, 27 Oct 2025 15:20:33 -0400 Subject: [PATCH 06/17] Type differs after init, sigh --- Sources/Testing/ExitTests/SpawnProcess.swift | 33 +++++++++++--------- 1 file changed, 18 insertions(+), 15 deletions(-) diff --git a/Sources/Testing/ExitTests/SpawnProcess.swift b/Sources/Testing/ExitTests/SpawnProcess.swift index 8c05b2393..088dfa604 100644 --- a/Sources/Testing/ExitTests/SpawnProcess.swift +++ b/Sources/Testing/ExitTests/SpawnProcess.swift @@ -72,23 +72,26 @@ func spawnExecutable( standardError: borrowing FileHandle? = nil, additionalFileHandles: [UnsafePointer] = [] ) throws -> ProcessID { - // Darwin and Linux differ in their optionality for the posix_spawn types we - // use, so use this typealias to paper over the differences. + // Darwin, the BSDs, Linux, and Android all differ in their optionality for + // the posix_spawn types we use, so use this typealias and helper function to + // paper over the differences. #if SWT_TARGET_OS_APPLE || os(FreeBSD) || os(OpenBSD) - typealias posix_spawn_file_actions_p = posix_spawn_file_actions_t? - typealias posix_spawnattr_p = posix_spawnattr_t? + typealias P = T? + func asArgument(_ p: UnsafeMutableBufferPointer) -> UnsafeMutablePointer { p.baseAddress! } #elseif os(Linux) - typealias posix_spawn_file_actions_p = posix_spawn_file_actions_t - typealias posix_spawnattr_p = posix_spawnattr_t + typealias P = T + func asArgument(_ p: UnsafeMutableBufferPointer) -> UnsafeMutablePointer { p.baseAddress! } #elseif os(Android) - typealias posix_spawn_file_actions_p = posix_spawn_file_actions_t? - typealias posix_spawnattr_p = posix_spawnattr_t + typealias P = T? + func asArgument(_ p: UnsafeMutableBufferPointer) -> UnsafeMutablePointer { + UnsafeMutableRawPointer(p.baseAddress!).bindMemory(to: T.self, capacity: 1) + } #endif -#if SWT_TARGET_OS_APPLE || os(Linux) || os(FreeBSD) || os(OpenBSD) || os(Android) - return try withUnsafeTemporaryAllocation(of: posix_spawn_file_actions_p.self, capacity: 1) { fileActions in - let fileActions = fileActions.baseAddress! - let fileActionsInitialized = posix_spawn_file_actions_init(fileActions) +#if SWT_TARGET_OS_APPLE || os(Linux) || os(FreeBSD) || os(OpenBSD) + return try withUnsafeTemporaryAllocation(of: P.self, capacity: 1) { fileActions in + let fileActionsInitialized = posix_spawn_file_actions_init(fileActions.baseAddress!) + let fileActions = asArgument(fileActions) guard 0 == fileActionsInitialized else { throw CError(rawValue: fileActionsInitialized) } @@ -96,9 +99,9 @@ func spawnExecutable( _ = posix_spawn_file_actions_destroy(fileActions) } - return try withUnsafeTemporaryAllocation(of: posix_spawnattr_p.self, capacity: 1) { attrs in - let attrs = attrs.baseAddress! - let attrsInitialized = posix_spawnattr_init(attrs) + return try withUnsafeTemporaryAllocation(of: P.self, capacity: 1) { attrs in + let attrsInitialized = posix_spawnattr_init(attrs.baseAddress!) + let attrs = asArgument(attrs) guard 0 == attrsInitialized else { throw CError(rawValue: attrsInitialized) } From 0eecc00e45cf031262b9c3e54fc25654a8d16943 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Mon, 27 Oct 2025 15:20:33 -0400 Subject: [PATCH 07/17] Expose si_pid and si_status (renaming in Swift doesn't seem to want to work on Android, cut the knot) --- Sources/Testing/ExitTests/WaitFor.swift | 6 +++--- Sources/_TestingInternals/include/Stubs.h | 10 ++++------ 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/Sources/Testing/ExitTests/WaitFor.swift b/Sources/Testing/ExitTests/WaitFor.swift index dc6f74ee9..cdf0e500d 100644 --- a/Sources/Testing/ExitTests/WaitFor.swift +++ b/Sources/Testing/ExitTests/WaitFor.swift @@ -29,9 +29,9 @@ private func _blockAndWait(for pid: consuming pid_t) throws -> ExitStatus { if 0 == waitid(P_PID, id_t(pid), &siginfo, WEXITED) { switch siginfo.si_code { case .init(CLD_EXITED): - return .exitCode(siginfo.si_status) + return .exitCode(swt_siginfo_t_si_status(siginfo)) case .init(CLD_KILLED), .init(CLD_DUMPED): - return .signal(siginfo.si_status) + return .signal(swt_siginfo_t_si_status(siginfo)) default: throw SystemError(description: "Unexpected siginfo_t value. Please file a bug report at https://github.com/swiftlang/swift-testing/issues/new and include this information: \(String(reflecting: siginfo))") } @@ -146,7 +146,7 @@ private let _createWaitThread: Void = { // continuation (if available) before reaping. var siginfo = siginfo_t() if 0 == waitid(P_ALL, 0, &siginfo, WEXITED | WNOWAIT) { - if case let pid = siginfo.si_pid, pid != 0 { + if case let pid = swt_siginfo_t_si_pid(siginfo), pid != 0 { let continuation = _withLockedChildProcessContinuations { childProcessContinuations, _ in childProcessContinuations.removeValue(forKey: pid) } diff --git a/Sources/_TestingInternals/include/Stubs.h b/Sources/_TestingInternals/include/Stubs.h index 1e743f2f2..ad96a85f4 100644 --- a/Sources/_TestingInternals/include/Stubs.h +++ b/Sources/_TestingInternals/include/Stubs.h @@ -132,9 +132,8 @@ static char *_Nullable *_Null_unspecified swt_environ(void) { /// This function is provided because `si_pid` is a complex macro on some /// platforms and cannot be imported directly into Swift. It is renamed back to /// `siginfo_t.si_pid` in Swift. -SWT_SWIFT_NAME(getter:siginfo_t.si_pid(self:)) -static pid_t swt_siginfo_t_si_pid(const siginfo_t *siginfo) { - return siginfo->si_pid; +static pid_t swt_siginfo_t_si_pid(siginfo_t siginfo) { + return siginfo.si_pid; } #endif @@ -144,9 +143,8 @@ static pid_t swt_siginfo_t_si_pid(const siginfo_t *siginfo) { /// This function is provided because `si_status` is a complex macro on some /// platforms and cannot be imported directly into Swift. It is renamed back to /// `siginfo_t.si_status` in Swift. -SWT_SWIFT_NAME(getter:siginfo_t.si_status(self:)) -static int swt_siginfo_t_si_status(const siginfo_t *siginfo) { - return siginfo->si_status; +static int swt_siginfo_t_si_status(siginfo_t siginfo) { + return siginfo.si_status; } #endif From b665223d5f1ffb03e7566c8c412fe494b263a566 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Mon, 27 Oct 2025 15:52:19 -0400 Subject: [PATCH 08/17] si_status and si_pid aren't macros on Darwin --- Sources/_TestingInternals/include/Stubs.h | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Sources/_TestingInternals/include/Stubs.h b/Sources/_TestingInternals/include/Stubs.h index ad96a85f4..a37f61089 100644 --- a/Sources/_TestingInternals/include/Stubs.h +++ b/Sources/_TestingInternals/include/Stubs.h @@ -126,7 +126,8 @@ static char *_Nullable *_Null_unspecified swt_environ(void) { } #endif -#if __has_include() && defined(si_pid) +#if !SWT_NO_PROCESS_SPAWNING && __has_include() +#if defined(__APPLE__) || defined(si_pid) /// Get the value of the `si_pid` field of a `siginfo_t` structure. /// /// This function is provided because `si_pid` is a complex macro on some @@ -137,7 +138,7 @@ static pid_t swt_siginfo_t_si_pid(siginfo_t siginfo) { } #endif -#if __has_include() && defined(si_status) +#if defined(__APPLE__) || defined(si_status) /// Get the value of the `si_status` field of a `siginfo_t` structure. /// /// This function is provided because `si_status` is a complex macro on some @@ -147,6 +148,7 @@ static int swt_siginfo_t_si_status(siginfo_t siginfo) { return siginfo.si_status; } #endif +#endif /// Get the value of `EEXIST`. /// From 7c6a47fcbd43a92bd2745260a627f602576dddcd Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Thu, 30 Oct 2025 14:42:22 -0400 Subject: [PATCH 09/17] Disable core files and tombstones --- Sources/Testing/ExitTests/ExitTest.swift | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/Sources/Testing/ExitTests/ExitTest.swift b/Sources/Testing/ExitTests/ExitTest.swift index 56d93a989..f7af55812 100644 --- a/Sources/Testing/ExitTests/ExitTest.swift +++ b/Sources/Testing/ExitTests/ExitTest.swift @@ -226,7 +226,20 @@ extension ExitTest { var rl = rlimit(rlim_cur: 0, rlim_max: 0) _ = setrlimit(RLIMIT_CORE, &rl) #elseif os(Android) - // TODO: "tombstoned_intercept"? + // Android inherits the RLIMIT_CORE=1 special case from Linux. + // SEE: https://android.googlesource.com/kernel/common/+/refs/heads/android-mainline/fs/coredump.c#978 + var rl = rlimit(rlim_cur: 1, rlim_max: 1) + _ = setrlimit(CInt(RLIMIT_CORE.rawValue), &rl) + + // In addition, Android installs signal handlers in native processes that + // cause the system to generate "tombstone" files. Suppress those too by + // resetting all signal handlers to SIG_DFL. debuggerd_register_handlers() + // is not exported, so we must manually walk all the signals it handles. + // SEE: https://android.googlesource.com/platform/system/core/+/main/debuggerd/include/debuggerd/handler.h#81 + let BIONIC_SIGNAL_DEBUGGER = __SIGRTMIN + 3 + for sig in [SIGABRT, SIGBUS, SIGFPE, SIGILL, SIGSEGV, SIGSTKFLT, SIGSYS, SIGTRAP, BIONIC_SIGNAL_DEBUGGER] { + _ = signal(sig, SIG_DFL) + } #elseif os(Windows) // On Windows, similarly disable Windows Error Reporting and the Windows // Error Reporting UI. Note we expect to be the first component to call From d64048d425b542749cae461bc7f9e2848ea93264 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Thu, 30 Oct 2025 15:08:48 -0400 Subject: [PATCH 10/17] Fixups --- Sources/Testing/ExitTests/ExitTest.swift | 4 ++-- Sources/_TestingInternals/include/Stubs.h | 15 +++++++++++---- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/Sources/Testing/ExitTests/ExitTest.swift b/Sources/Testing/ExitTests/ExitTest.swift index f7af55812..325661f10 100644 --- a/Sources/Testing/ExitTests/ExitTest.swift +++ b/Sources/Testing/ExitTests/ExitTest.swift @@ -229,7 +229,7 @@ extension ExitTest { // Android inherits the RLIMIT_CORE=1 special case from Linux. // SEE: https://android.googlesource.com/kernel/common/+/refs/heads/android-mainline/fs/coredump.c#978 var rl = rlimit(rlim_cur: 1, rlim_max: 1) - _ = setrlimit(CInt(RLIMIT_CORE.rawValue), &rl) + _ = setrlimit(RLIMIT_CORE, &rl) // In addition, Android installs signal handlers in native processes that // cause the system to generate "tombstone" files. Suppress those too by @@ -238,7 +238,7 @@ extension ExitTest { // SEE: https://android.googlesource.com/platform/system/core/+/main/debuggerd/include/debuggerd/handler.h#81 let BIONIC_SIGNAL_DEBUGGER = __SIGRTMIN + 3 for sig in [SIGABRT, SIGBUS, SIGFPE, SIGILL, SIGSEGV, SIGSTKFLT, SIGSYS, SIGTRAP, BIONIC_SIGNAL_DEBUGGER] { - _ = signal(sig, SIG_DFL) + _ = signal(sig, swt_SIG_DFL()) } #elseif os(Windows) // On Windows, similarly disable Windows Error Reporting and the Windows diff --git a/Sources/_TestingInternals/include/Stubs.h b/Sources/_TestingInternals/include/Stubs.h index a37f61089..e09114458 100644 --- a/Sources/_TestingInternals/include/Stubs.h +++ b/Sources/_TestingInternals/include/Stubs.h @@ -131,8 +131,7 @@ static char *_Nullable *_Null_unspecified swt_environ(void) { /// Get the value of the `si_pid` field of a `siginfo_t` structure. /// /// This function is provided because `si_pid` is a complex macro on some -/// platforms and cannot be imported directly into Swift. It is renamed back to -/// `siginfo_t.si_pid` in Swift. +/// platforms and cannot be imported directly into Swift. static pid_t swt_siginfo_t_si_pid(siginfo_t siginfo) { return siginfo.si_pid; } @@ -142,12 +141,20 @@ static pid_t swt_siginfo_t_si_pid(siginfo_t siginfo) { /// Get the value of the `si_status` field of a `siginfo_t` structure. /// /// This function is provided because `si_status` is a complex macro on some -/// platforms and cannot be imported directly into Swift. It is renamed back to -/// `siginfo_t.si_status` in Swift. +/// platforms and cannot be imported directly into Swift. static int swt_siginfo_t_si_status(siginfo_t siginfo) { return siginfo.si_status; } #endif + +/// Get the default signal handler. +/// +/// This function is provided because `SIG_DFL` is a complex macro on some +/// platforms and cannot be imported directly into Swift. +static sig_t swt_SIG_DFL(void) { + return SIG_DFL; +} + #endif /// Get the value of `EEXIST`. From c3f216367189d52f47fa4374951572a00dd4e7d8 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Thu, 30 Oct 2025 15:11:20 -0400 Subject: [PATCH 11/17] Fix platform-specific missing bits --- Sources/Testing/ExitTests/ExitTest.swift | 2 +- Sources/Testing/ExitTests/SpawnProcess.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/Testing/ExitTests/ExitTest.swift b/Sources/Testing/ExitTests/ExitTest.swift index 325661f10..5d7891d7b 100644 --- a/Sources/Testing/ExitTests/ExitTest.swift +++ b/Sources/Testing/ExitTests/ExitTest.swift @@ -679,7 +679,7 @@ extension ExitTest { Environment.setVariable(nil, named: name) var fd: CInt? -#if SWT_TARGET_OS_APPLE || os(Linux) || os(FreeBSD) || os(OpenBSD) +#if SWT_TARGET_OS_APPLE || os(Linux) || os(FreeBSD) || os(OpenBSD) || os(Android) fd = CInt(environmentVariable) #elseif os(Windows) if let handle = UInt(environmentVariable).flatMap(HANDLE.init(bitPattern:)) { diff --git a/Sources/Testing/ExitTests/SpawnProcess.swift b/Sources/Testing/ExitTests/SpawnProcess.swift index 088dfa604..8c0241a28 100644 --- a/Sources/Testing/ExitTests/SpawnProcess.swift +++ b/Sources/Testing/ExitTests/SpawnProcess.swift @@ -88,7 +88,7 @@ func spawnExecutable( } #endif -#if SWT_TARGET_OS_APPLE || os(Linux) || os(FreeBSD) || os(OpenBSD) +#if SWT_TARGET_OS_APPLE || os(Linux) || os(FreeBSD) || os(OpenBSD) || os(Android) return try withUnsafeTemporaryAllocation(of: P.self, capacity: 1) { fileActions in let fileActionsInitialized = posix_spawn_file_actions_init(fileActions.baseAddress!) let fileActions = asArgument(fileActions) From e297fd4fe4bc0790b87bbd277ed6f084733c712e Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Thu, 30 Oct 2025 15:33:08 -0400 Subject: [PATCH 12/17] More fixups --- Sources/Testing/ExitTests/SpawnProcess.swift | 35 +++++++++++++------- 1 file changed, 23 insertions(+), 12 deletions(-) diff --git a/Sources/Testing/ExitTests/SpawnProcess.swift b/Sources/Testing/ExitTests/SpawnProcess.swift index 8c0241a28..14cc36572 100644 --- a/Sources/Testing/ExitTests/SpawnProcess.swift +++ b/Sources/Testing/ExitTests/SpawnProcess.swift @@ -77,21 +77,14 @@ func spawnExecutable( // paper over the differences. #if SWT_TARGET_OS_APPLE || os(FreeBSD) || os(OpenBSD) typealias P = T? - func asArgument(_ p: UnsafeMutableBufferPointer) -> UnsafeMutablePointer { p.baseAddress! } -#elseif os(Linux) +#elseif os(Linux) || os(Android) typealias P = T - func asArgument(_ p: UnsafeMutableBufferPointer) -> UnsafeMutablePointer { p.baseAddress! } -#elseif os(Android) - typealias P = T? - func asArgument(_ p: UnsafeMutableBufferPointer) -> UnsafeMutablePointer { - UnsafeMutableRawPointer(p.baseAddress!).bindMemory(to: T.self, capacity: 1) - } #endif #if SWT_TARGET_OS_APPLE || os(Linux) || os(FreeBSD) || os(OpenBSD) || os(Android) return try withUnsafeTemporaryAllocation(of: P.self, capacity: 1) { fileActions in - let fileActionsInitialized = posix_spawn_file_actions_init(fileActions.baseAddress!) - let fileActions = asArgument(fileActions) + let fileActions = fileActions.baseAddress! + let fileActionsInitialized = posix_spawn_file_actions_init(fileActions) guard 0 == fileActionsInitialized else { throw CError(rawValue: fileActionsInitialized) } @@ -100,8 +93,14 @@ func spawnExecutable( } return try withUnsafeTemporaryAllocation(of: P.self, capacity: 1) { attrs in - let attrsInitialized = posix_spawnattr_init(attrs.baseAddress!) - let attrs = asArgument(attrs) + let attrs = attrs.baseAddress! +#if os(Android) + let attrsInitialized = attrs.withMemoryRebound(to: Optional.self, capacity: 1) { attrs in + posix_spawnattr_init(attrs) + } +#else + let attrsInitialized = posix_spawnattr_init(attrs) +#endif guard 0 == attrsInitialized else { throw CError(rawValue: attrsInitialized) } @@ -237,7 +236,19 @@ func spawnExecutable( } var pid = pid_t() +#if os(Android) + let processSpawned = fileActions.withMemoryRebound(to: posix_spawn_file_actions_t?.self, capacity: 1) { fileActions in + attrs.withMemoryRebound(to: posix_spawnattr_t?.self, capacity: 1) { attrs in + argv.withUnsafeBufferPointer { argv in + argv.withMemoryRebound(to: UnsafeMutablePointer.self) { argv in + posix_spawn(&pid, executablePath, fileActions, attrs, argv.baseAddress!, environ) + } + } + } + } +#else let processSpawned = posix_spawn(&pid, executablePath, fileActions, attrs, argv, environ) +#endif guard 0 == processSpawned else { throw CError(rawValue: processSpawned) } From 8a1c30bf39ea8594ad3d5a1e9f45ed5df5c92614 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Thu, 30 Oct 2025 16:12:12 -0400 Subject: [PATCH 13/17] Work around posix_spawn nullability problems --- Package.swift | 17 +++++++++++++++-- Sources/Testing/ExitTests/SpawnProcess.swift | 10 +--------- Sources/_TestingInternals/include/Stubs.h | 14 ++++++++++++++ 3 files changed, 30 insertions(+), 11 deletions(-) diff --git a/Package.swift b/Package.swift index 92b4c011f..a858b88d8 100644 --- a/Package.swift +++ b/Package.swift @@ -363,6 +363,19 @@ extension BuildSettingCondition { } } +/// A constant set of platforms including Android when the compiler is 6.3 or +/// newer. +/// +/// This constant is used to conditionally enable features on Android only on +/// newer compilers. +static let androidIfCompiler6_3: some Collection = { +#if compiler(>=6.3) + CollectionOfOne(.android) +#else + EmptyCollection() +#endif +}() + extension Array where Element == PackageDescription.SwiftSetting { /// Settings intended to be applied to every Swift target in this package. /// Analogous to project-level build settings in an Xcode project. @@ -398,8 +411,8 @@ extension Array where Element == PackageDescription.SwiftSetting { .define("SWT_TARGET_OS_APPLE", .whenApple()), - .define("SWT_NO_EXIT_TESTS", .whenEmbedded(or: .when(platforms: [.iOS, .watchOS, .tvOS, .visionOS, .wasi]))), - .define("SWT_NO_PROCESS_SPAWNING", .whenEmbedded(or: .when(platforms: [.iOS, .watchOS, .tvOS, .visionOS, .wasi]))), + .define("SWT_NO_EXIT_TESTS", .whenEmbedded(or: .when(platforms: [.iOS, .watchOS, .tvOS, .visionOS, .wasi] + androidIfCompiler6_3))), + .define("SWT_NO_PROCESS_SPAWNING", .whenEmbedded(or: .when(platforms: [.iOS, .watchOS, .tvOS, .visionOS, .wasi] + androidIfCompiler6_3))), .define("SWT_NO_SNAPSHOT_TYPES", .whenEmbedded(or: .whenApple(false))), .define("SWT_NO_DYNAMIC_LINKING", .whenEmbedded(or: .when(platforms: [.wasi]))), .define("SWT_NO_PIPES", .whenEmbedded(or: .when(platforms: [.wasi]))), diff --git a/Sources/Testing/ExitTests/SpawnProcess.swift b/Sources/Testing/ExitTests/SpawnProcess.swift index 14cc36572..063872336 100644 --- a/Sources/Testing/ExitTests/SpawnProcess.swift +++ b/Sources/Testing/ExitTests/SpawnProcess.swift @@ -237,15 +237,7 @@ func spawnExecutable( var pid = pid_t() #if os(Android) - let processSpawned = fileActions.withMemoryRebound(to: posix_spawn_file_actions_t?.self, capacity: 1) { fileActions in - attrs.withMemoryRebound(to: posix_spawnattr_t?.self, capacity: 1) { attrs in - argv.withUnsafeBufferPointer { argv in - argv.withMemoryRebound(to: UnsafeMutablePointer.self) { argv in - posix_spawn(&pid, executablePath, fileActions, attrs, argv.baseAddress!, environ) - } - } - } - } + let processSpawned = swt_posix_spawn(&pid, executablePath, fileActions, attrs, argv, environ) #else let processSpawned = posix_spawn(&pid, executablePath, fileActions, attrs, argv, environ) #endif diff --git a/Sources/_TestingInternals/include/Stubs.h b/Sources/_TestingInternals/include/Stubs.h index e09114458..51731bc8c 100644 --- a/Sources/_TestingInternals/include/Stubs.h +++ b/Sources/_TestingInternals/include/Stubs.h @@ -155,6 +155,20 @@ static sig_t swt_SIG_DFL(void) { return SIG_DFL; } +#if defined(__ANDROID__) +/// Call `posix_spawn(3)`. +/// +/// This function is provided because the nullability for `posix_spawn(3)` is +/// incorrectly specified in the Android NDK. +static int swt_posix_spawn( + pid_t *pid, const char *path, + const posix_spawn_file_actions_t _Nonnull *_Nullable fileActions, + const posix_spawnattr_t _Nonnull *_Nullable attrs, + char *const _Nullable argv[_Nonnull], char *const _Nullable env[_Nonnull] +) { + return posix_spawn(pid, path, fileActions, attrs, argv, env); +} +#endif #endif /// Get the value of `EEXIST`. From 947ad22f5ae65ddaccd32191621e94154110cb5d Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Thu, 30 Oct 2025 16:13:19 -0400 Subject: [PATCH 14/17] Optional -> ? --- Sources/Testing/ExitTests/SpawnProcess.swift | 2 +- foo.txt | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) create mode 100644 foo.txt diff --git a/Sources/Testing/ExitTests/SpawnProcess.swift b/Sources/Testing/ExitTests/SpawnProcess.swift index 14cc36572..b4ba73c16 100644 --- a/Sources/Testing/ExitTests/SpawnProcess.swift +++ b/Sources/Testing/ExitTests/SpawnProcess.swift @@ -95,7 +95,7 @@ func spawnExecutable( return try withUnsafeTemporaryAllocation(of: P.self, capacity: 1) { attrs in let attrs = attrs.baseAddress! #if os(Android) - let attrsInitialized = attrs.withMemoryRebound(to: Optional.self, capacity: 1) { attrs in + let attrsInitialized = attrs.withMemoryRebound(to: posix_spawnattr_t?.self, capacity: 1) { attrs in posix_spawnattr_init(attrs) } #else diff --git a/foo.txt b/foo.txt new file mode 100644 index 000000000..e5e3b6e74 --- /dev/null +++ b/foo.txt @@ -0,0 +1,2 @@ +int posix_spawn(pid_t* _Nullable __pid, const char* _Nonnull __path, const posix_spawn_file_actions_t _Nullable * _Nullable __actions, const posix_spawnattr_t _Nullable * _Nullable __attr, char* const _Nonnull __argv[_Nonnull], char* const _Nullable __env[_Nullable]) __INTRODUCED_IN(28); +int posix_spawnp(pid_t* _Nullable __pid, const char* _Nonnull __file, const posix_spawn_file_actions_t _Nullable * _Nullable __actions, const posix_spawnattr_t _Nullable * _Nullable __attr, char* const _Nonnull __argv[_Nonnull], char* const _Nullable __env[_Nullable]) __INTRODUCED_IN(28); From c6fe393b63fa504da78b57ea7dcad20b75c78a17 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Thu, 30 Oct 2025 16:14:24 -0400 Subject: [PATCH 15/17] Fix androidIfCompiler6_3 sigh --- Package.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Package.swift b/Package.swift index a858b88d8..6cde28ea4 100644 --- a/Package.swift +++ b/Package.swift @@ -368,13 +368,13 @@ extension BuildSettingCondition { /// /// This constant is used to conditionally enable features on Android only on /// newer compilers. -static let androidIfCompiler6_3: some Collection = { +nonisolated(unsafe) var androidIfCompiler6_3: some Collection { #if compiler(>=6.3) CollectionOfOne(.android) #else EmptyCollection() #endif -}() +} extension Array where Element == PackageDescription.SwiftSetting { /// Settings intended to be applied to every Swift target in this package. From e9fc4539ca1eece7da1ea5879ac388b142d8e8b5 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Thu, 30 Oct 2025 16:17:52 -0400 Subject: [PATCH 16/17] Add nullable --- Sources/_TestingInternals/include/Stubs.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/_TestingInternals/include/Stubs.h b/Sources/_TestingInternals/include/Stubs.h index 51731bc8c..afbc5204d 100644 --- a/Sources/_TestingInternals/include/Stubs.h +++ b/Sources/_TestingInternals/include/Stubs.h @@ -161,7 +161,7 @@ static sig_t swt_SIG_DFL(void) { /// This function is provided because the nullability for `posix_spawn(3)` is /// incorrectly specified in the Android NDK. static int swt_posix_spawn( - pid_t *pid, const char *path, + pid_t *_Nullable pid, const char *path, const posix_spawn_file_actions_t _Nonnull *_Nullable fileActions, const posix_spawnattr_t _Nonnull *_Nullable attrs, char *const _Nullable argv[_Nonnull], char *const _Nullable env[_Nonnull] From fc7023c85ee6e1e93fb606674d4c07b269144b57 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Thu, 30 Oct 2025 16:18:43 -0400 Subject: [PATCH 17/17] Add _Null_unspecified --- Sources/_TestingInternals/include/Stubs.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/_TestingInternals/include/Stubs.h b/Sources/_TestingInternals/include/Stubs.h index afbc5204d..130cde4fb 100644 --- a/Sources/_TestingInternals/include/Stubs.h +++ b/Sources/_TestingInternals/include/Stubs.h @@ -151,7 +151,7 @@ static int swt_siginfo_t_si_status(siginfo_t siginfo) { /// /// This function is provided because `SIG_DFL` is a complex macro on some /// platforms and cannot be imported directly into Swift. -static sig_t swt_SIG_DFL(void) { +static sig_t _Null_unspecified swt_SIG_DFL(void) { return SIG_DFL; }