Skip to content

Use our posix_spawn() wrapper in the Foundation CIO rather than Process. #1114

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

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
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
3 changes: 3 additions & 0 deletions Documentation/Porting.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,9 @@ platform-specific attention.
> conflicting requirements (for example, attempting to enable support for pipes
> without also enabling support for file I/O.) You should be able to resolve
> these issues by updating `Package.swift` and/or `CompilerSettings.cmake`.
>
> Don't forget to add your platform to the `BuildSettingCondition/whenApple(_:)`
> function in `Package.swift`.

Most platform dependencies can be resolved through the use of platform-specific
API. For example, Swift Testing uses the C11 standard [`timespec`](https://en.cppreference.com/w/c/chrono/timespec)
Expand Down
47 changes: 30 additions & 17 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ let buildingForDevelopment = (git?.currentTag == nil)
/// to change in the future.
///
/// - Bug: There is currently no way for us to tell if we are being asked to
/// build for an Embedded Swift target at the package manifest level.
/// build for an Embedded Swift target at the package manifest level.
/// ([swift-syntax-#8431](https://github.com/swiftlang/swift-package-manager/issues/8431))
let buildingForEmbedded: Bool = {
guard let envvar = Context.environment["SWT_EMBEDDED"] else {
Expand Down Expand Up @@ -193,7 +193,7 @@ let package = Package(
// The Foundation module only has Library Evolution enabled on Apple
// platforms, and since this target's module publicly imports Foundation,
// it can only enable Library Evolution itself on those platforms.
swiftSettings: .packageSettings + .enableLibraryEvolution(applePlatformsOnly: true)
swiftSettings: .packageSettings + .enableLibraryEvolution(.whenApple())
),

// Utility targets: These are utilities intended for use when developing
Expand Down Expand Up @@ -229,11 +229,11 @@ extension BuildSettingCondition {
/// Swift.
///
/// - Parameters:
/// - nonEmbeddedCondition: The value to return if the target is not
/// Embedded Swift. If `nil`, the build condition evaluates to `false`.
/// - nonEmbeddedCondition: The value to return if the target is not
/// Embedded Swift. If `nil`, the build condition evaluates to `false`.
///
/// - Returns: A build setting condition that evaluates to `true` for Embedded
/// Swift or is equal to `nonEmbeddedCondition` for non-Embedded Swift.
/// Swift or is equal to `nonEmbeddedCondition` for non-Embedded Swift.
static func whenEmbedded(or nonEmbeddedCondition: @autoclosure () -> Self? = nil) -> Self? {
if !buildingForEmbedded {
if let nonEmbeddedCondition = nonEmbeddedCondition() {
Expand All @@ -248,6 +248,21 @@ extension BuildSettingCondition {
nil
}
}

/// A build setting condition representing all Apple or non-Apple platforms.
///
/// - Parameters:
/// - isApple: Whether or not the result represents Apple platforms.
///
/// - Returns: A build setting condition that evaluates to `isApple` for Apple
/// platforms.
static func whenApple(_ isApple: Bool = true) -> Self {
if isApple {
.when(platforms: [.macOS, .iOS, .macCatalyst, .watchOS, .tvOS, .visionOS])
} else {
.when(platforms: [.linux, .custom("freebsd"), .openbsd, .windows, .wasi, .android])
}
}
}

extension Array where Element == PackageDescription.SwiftSetting {
Expand Down Expand Up @@ -292,13 +307,14 @@ extension Array where Element == PackageDescription.SwiftSetting {
// executable rather than a library.
.define("SWT_NO_LIBRARY_MACRO_PLUGINS"),

.define("SWT_TARGET_OS_APPLE", .when(platforms: [.macOS, .iOS, .macCatalyst, .watchOS, .tvOS, .visionOS])),
.define("SWT_TARGET_OS_APPLE", .whenApple()),

.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_SNAPSHOT_TYPES", .whenEmbedded(or: .when(platforms: [.linux, .custom("freebsd"), .openbsd, .windows, .wasi, .android]))),
.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]))),
.define("SWT_NO_FOUNDATION_FILE_COORDINATION", .whenEmbedded(or: .whenApple(false))),

.define("SWT_NO_LEGACY_TEST_DISCOVERY", .whenEmbedded()),
.define("SWT_NO_LIBDISPATCH", .whenEmbedded()),
Expand Down Expand Up @@ -334,20 +350,16 @@ extension Array where Element == PackageDescription.SwiftSetting {
]
}

/// Create a Swift setting which enables Library Evolution, optionally
/// constraining it to only Apple platforms.
/// Create a Swift setting which enables Library Evolution.
///
/// - Parameters:
/// - applePlatformsOnly: Whether to constrain this setting to only Apple
/// platforms.
static func enableLibraryEvolution(applePlatformsOnly: Bool = false) -> Self {
/// - condition: A build setting condition to apply to this setting.
///
/// - Returns: A Swift setting that enables Library Evolution.
static func enableLibraryEvolution(_ condition: BuildSettingCondition? = nil) -> Self {
var result = [PackageDescription.SwiftSetting]()

if buildingForDevelopment {
var condition: BuildSettingCondition?
if applePlatformsOnly {
condition = .when(platforms: [.macOS, .iOS, .macCatalyst, .watchOS, .tvOS, .visionOS])
}
result.append(.unsafeFlags(["-enable-library-evolution"], condition))
}

Expand All @@ -364,9 +376,10 @@ 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_SNAPSHOT_TYPES", .whenEmbedded(or: .when(platforms: [.linux, .custom("freebsd"), .openbsd, .windows, .wasi, .android]))),
.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]))),
.define("SWT_NO_FOUNDATION_FILE_COORDINATION", .whenEmbedded(or: .whenApple(false))),

.define("SWT_NO_LEGACY_TEST_DISCOVERY", .whenEmbedded()),
.define("SWT_NO_LIBDISPATCH", .whenEmbedded()),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ extension Attachment where AttachableValue == _AttachableURLWrapper {
let url = url.resolvingSymlinksInPath()
let isDirectory = try url.resourceValues(forKeys: [.isDirectoryKey]).isDirectory!

#if SWT_TARGET_OS_APPLE
#if SWT_TARGET_OS_APPLE && !SWT_NO_FOUNDATION_FILE_COORDINATION
let data: Data = try await withCheckedThrowingContinuation { continuation in
let fileCoordinator = NSFileCoordinator()
let fileAccessIntent = NSFileAccessIntent.readingIntent(with: url, options: [.forUploading])
Expand Down Expand Up @@ -165,25 +165,31 @@ private func _compressContentsOfDirectory(at directoryURL: URL) async throws ->
// knows how to write PKZIP archives, while Windows inherited FreeBSD's tar
// tool in Windows 10 Build 17063 (per https://techcommunity.microsoft.com/blog/containers/tar-and-curl-come-to-windows/382409).
//
// On Linux (which does not have FreeBSD's version of tar(1)), we can use
// zip(1) instead.
// On Linux and OpenBSD (which do not have FreeBSD's version of tar(1)), we
// can use zip(1) instead. This tool compresses paths relative to the current
// working directory, and posix_spawn_file_actions_addchdir_np() is not always
// available for us to call (not present on OpenBSD, requires glibc ≥ 2.28 on
// Linux), so we'll spawn a shell that calls cd before calling zip(1).
//
// 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)
let archiverPath = "/bin/sh"
#if os(Linux)
let archiverPath = "/usr/bin/zip"
#elseif SWT_TARGET_OS_APPLE || os(FreeBSD)
let archiverPath = "/usr/bin/tar"
#elseif os(OpenBSD)
let archiverPath = "/usr/local/bin/zip"
let trueArchiverPath = "/usr/bin/zip"
#else
let trueArchiverPath = "/usr/local/bin/zip"
var isDirectory = false
if !FileManager.default.fileExists(atPath: archiverPath, isDirectory: &isDirectory) || isDirectory {
if !FileManager.default.fileExists(atPath: trueArchiverPath, isDirectory: &isDirectory) || isDirectory {
throw CocoaError(.fileNoSuchFile, userInfo: [
NSLocalizedDescriptionKey: "The 'zip' package is not installed.",
NSFilePathErrorKey: archiverPath
NSFilePathErrorKey: trueArchiverPath
])
}
#endif
#elseif SWT_TARGET_OS_APPLE || os(FreeBSD)
let archiverPath = "/usr/bin/tar"
#elseif os(Windows)
guard let archiverPath = _archiverPath else {
throw CocoaError(.fileWriteUnknown, userInfo: [
Expand All @@ -196,20 +202,15 @@ private func _compressContentsOfDirectory(at directoryURL: URL) async throws ->
throw CocoaError(.featureUnsupported, userInfo: [NSLocalizedDescriptionKey: "This platform does not support attaching directories to tests."])
#endif

try await withCheckedThrowingContinuation { continuation in
let process = Process()

process.executableURL = URL(fileURLWithPath: archiverPath, isDirectory: false)

let sourcePath = directoryURL.fileSystemPath
let destinationPath = temporaryURL.fileSystemPath
let sourcePath = directoryURL.fileSystemPath
let destinationPath = temporaryURL.fileSystemPath
let arguments = {
#if os(Linux) || os(OpenBSD)
// The zip command constructs relative paths from the current working
// directory rather than from command-line arguments.
process.arguments = [destinationPath, "--recurse-paths", "."]
process.currentDirectoryURL = directoryURL
["-c", #"cd "$0" && "$1" "$2" --recurse-paths ."#, sourcePath, trueArchiverPath, destinationPath]
#elseif SWT_TARGET_OS_APPLE || os(FreeBSD)
process.arguments = ["--create", "--auto-compress", "--directory", sourcePath, "--file", destinationPath, "."]
["--create", "--auto-compress", "--directory", sourcePath, "--file", destinationPath, "."]
#elseif os(Windows)
// The Windows version of bsdtar can handle relative paths for other archive
// formats, but produces empty archives when inferring the zip format with
Expand All @@ -218,30 +219,15 @@ private func _compressContentsOfDirectory(at directoryURL: URL) async throws ->
// An alternative may be to use PowerShell's Compress-Archive command,
// however that comes with a security risk as we'd be responsible for two
// levels of command-line argument escaping.
process.arguments = ["--create", "--auto-compress", "--file", destinationPath, sourcePath]
["--create", "--auto-compress", "--file", destinationPath, sourcePath]
#endif
}()

process.standardOutput = nil
process.standardError = nil

process.terminationHandler = { process in
let terminationReason = process.terminationReason
let terminationStatus = process.terminationStatus
if terminationReason == .exit && terminationStatus == EXIT_SUCCESS {
continuation.resume()
} else {
let error = CocoaError(.fileWriteUnknown, userInfo: [
NSLocalizedDescriptionKey: "The directory at '\(sourcePath)' could not be compressed (\(terminationStatus)).",
])
continuation.resume(throwing: error)
}
}

do {
try process.run()
} catch {
continuation.resume(throwing: error)
}
let exitStatus = try await spawnExecutableAtPathAndWait(archiverPath, arguments: arguments)
guard case .exitCode(EXIT_SUCCESS) = exitStatus else {
throw CocoaError(.fileWriteUnknown, userInfo: [
NSLocalizedDescriptionKey: "The directory at '\(sourcePath)' could not be compressed (\(exitStatus)).",
])
}

return try Data(contentsOf: temporaryURL, options: [.mappedIfSafe])
Expand Down
40 changes: 33 additions & 7 deletions Sources/Testing/ExitTests/SpawnProcess.swift
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ private let _posix_spawn_file_actions_addclosefrom_np = symbol(named: "posix_spa
}
#endif

/// Spawn a process and wait for it to terminate.
/// Spawn a child process.
///
/// - Parameters:
/// - executablePath: The path to the executable to spawn.
Expand All @@ -61,8 +61,7 @@ private let _posix_spawn_file_actions_addclosefrom_np = symbol(named: "posix_spa
/// eventually pass this value to ``wait(for:)`` to avoid leaking system
/// resources.
///
/// - Throws: Any error that prevented the process from spawning or its exit
/// condition from being read.
/// - Throws: Any error that prevented the process from spawning.
func spawnExecutable(
atPath executablePath: String,
arguments: [String],
Expand All @@ -83,17 +82,19 @@ func spawnExecutable(
#if SWT_TARGET_OS_APPLE || os(Linux) || os(FreeBSD) || os(OpenBSD)
return try withUnsafeTemporaryAllocation(of: P<posix_spawn_file_actions_t>.self, capacity: 1) { fileActions in
let fileActions = fileActions.baseAddress!
guard 0 == posix_spawn_file_actions_init(fileActions) else {
throw CError(rawValue: swt_errno())
let fileActionsInitialized = posix_spawn_file_actions_init(fileActions)
guard 0 == fileActionsInitialized else {
throw CError(rawValue: fileActionsInitialized)
}
defer {
_ = posix_spawn_file_actions_destroy(fileActions)
}

return try withUnsafeTemporaryAllocation(of: P<posix_spawnattr_t>.self, capacity: 1) { attrs in
let attrs = attrs.baseAddress!
guard 0 == posix_spawnattr_init(attrs) else {
throw CError(rawValue: swt_errno())
let attrsInitialized = posix_spawnattr_init(attrs)
guard 0 == attrsInitialized else {
throw CError(rawValue: attrsInitialized)
}
defer {
_ = posix_spawnattr_destroy(attrs)
Expand Down Expand Up @@ -396,4 +397,29 @@ private func _escapeCommandLine(_ arguments: [String]) -> String {
}.joined(separator: " ")
}
#endif

/// Spawn a child process and wait for it to terminate.
///
/// - Parameters:
/// - executablePath: The path to the executable to spawn.
/// - arguments: The arguments to pass to the executable, not including the
/// executable path.
/// - environment: The environment block to pass to the executable.
///
/// - Returns: The exit status of the spawned process.
///
/// - Throws: Any error that prevented the process from spawning or its exit
/// condition from being read.
///
/// 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.
package func spawnExecutableAtPathAndWait(
_ executablePath: String,
arguments: [String] = [],
environment: [String: String] = [:]
) async throws -> ExitStatus {
let processID = try spawnExecutable(atPath: executablePath, arguments: arguments, environment: environment)
return try await wait(for: processID)
}
#endif
1 change: 1 addition & 0 deletions cmake/modules/shared/CompilerSettings.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ if(CMAKE_SYSTEM_NAME IN_LIST SWT_NO_PROCESS_SPAWNING_LIST)
endif()
if(NOT APPLE)
add_compile_definitions("SWT_NO_SNAPSHOT_TYPES")
add_compile_definitions("SWT_NO_FOUNDATION_FILE_COORDINATION")
endif()
if(CMAKE_SYSTEM_NAME STREQUAL "WASI")
add_compile_definitions("SWT_NO_DYNAMIC_LINKING")
Expand Down