diff --git a/Documentation/Porting.md b/Documentation/Porting.md index 6e83e0eb0..f7aecf97e 100644 --- a/Documentation/Porting.md +++ b/Documentation/Porting.md @@ -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) diff --git a/Package.swift b/Package.swift index 44116b6d1..050ea25ac 100644 --- a/Package.swift +++ b/Package.swift @@ -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 { @@ -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 @@ -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() { @@ -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 { @@ -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()), @@ -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)) } @@ -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()), diff --git a/Sources/Overlays/_Testing_Foundation/Attachments/Attachment+URL.swift b/Sources/Overlays/_Testing_Foundation/Attachments/Attachment+URL.swift index 83c3909be..3c101c09c 100644 --- a/Sources/Overlays/_Testing_Foundation/Attachments/Attachment+URL.swift +++ b/Sources/Overlays/_Testing_Foundation/Attachments/Attachment+URL.swift @@ -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]) @@ -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: [ @@ -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 @@ -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]) diff --git a/Sources/Testing/ExitTests/SpawnProcess.swift b/Sources/Testing/ExitTests/SpawnProcess.swift index 8f8d95db6..c365f33ea 100644 --- a/Sources/Testing/ExitTests/SpawnProcess.swift +++ b/Sources/Testing/ExitTests/SpawnProcess.swift @@ -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. @@ -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], @@ -83,8 +82,9 @@ func spawnExecutable( #if SWT_TARGET_OS_APPLE || os(Linux) || os(FreeBSD) || os(OpenBSD) return try withUnsafeTemporaryAllocation(of: P.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) @@ -92,8 +92,9 @@ func spawnExecutable( return try withUnsafeTemporaryAllocation(of: P.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) @@ -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 diff --git a/cmake/modules/shared/CompilerSettings.cmake b/cmake/modules/shared/CompilerSettings.cmake index 0da4216c5..cd18e0062 100644 --- a/cmake/modules/shared/CompilerSettings.cmake +++ b/cmake/modules/shared/CompilerSettings.cmake @@ -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")