Skip to content
Open
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
1 change: 1 addition & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ add_compile_definitions(USE_STATIC_PLUGIN_INITIALIZATION)
find_package(ArgumentParser)
find_package(LLBuild)
find_package(SwiftDriver)
find_package(SwiftSubprocess)
find_package(SwiftSystem)
find_package(TSC)
# NOTE: these two are required for LLBuild dependencies
Expand Down
3 changes: 3 additions & 0 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,7 @@ let package = Package(
"SWBCSupport",
"SWBLibc",
.product(name: "ArgumentParser", package: "swift-argument-parser"),
.product(name: "Subprocess", package: "swift-subprocess", condition: .when(platforms: [.android, .custom("freebsd"), .linux, .macOS, .openbsd, .windows])),
.product(name: "SystemPackage", package: "swift-system", condition: .when(platforms: [.linux, .openbsd, .android, .windows, .custom("freebsd")])),
],
exclude: ["CMakeLists.txt"],
Expand Down Expand Up @@ -458,6 +459,7 @@ if isStaticBuild {
if useLocalDependencies {
package.dependencies += [
.package(path: "../swift-driver"),
.package(path: "../swift-subprocess"),
.package(path: "../swift-system"),
.package(path: "../swift-argument-parser"),
]
Expand All @@ -467,6 +469,7 @@ if useLocalDependencies {
} else {
package.dependencies += [
.package(url: "https://github.com/swiftlang/swift-driver.git", branch: "main"),
.package(url: "https://github.com/swiftlang/swift-subprocess.git", .upToNextMinor(from: "0.1.0")),
.package(url: "https://github.com/apple/swift-system.git", .upToNextMajor(from: "1.5.0")),
.package(url: "https://github.com/apple/swift-argument-parser.git", from: "1.0.3"),
]
Expand Down
11 changes: 10 additions & 1 deletion Plugins/cmake-smoke-test/cmake-smoke-test.swift
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,9 @@ struct CMakeSmokeTest: CommandPlugin {
let swiftToolsSupportCoreURL = try findDependency("swift-tools-support-core", pluginContext: context)
let swiftToolsSupportCoreBuildURL = context.pluginWorkDirectoryURL.appending(component: "swift-tools-support-core")

let swiftSubprocessURL = try findDependency("swift-subprocess", pluginContext: context)
let swiftSubprocessBuildURL = context.pluginWorkDirectoryURL.appending(component: "swift-subprocess")

let swiftSystemURL = try findDependency("swift-system", pluginContext: context)
let swiftSystemBuildURL = context.pluginWorkDirectoryURL.appending(component: "swift-system")

Expand All @@ -58,7 +61,7 @@ struct CMakeSmokeTest: CommandPlugin {
let swiftDriverURL = try findDependency("swift-driver", pluginContext: context)
let swiftDriverBuildURL = context.pluginWorkDirectoryURL.appending(component: "swift-driver")

for url in [swiftToolsSupportCoreBuildURL, swiftSystemBuildURL, llbuildBuildURL, swiftArgumentParserBuildURL, swiftDriverBuildURL, swiftBuildBuildURL] {
for url in [swiftToolsSupportCoreBuildURL, swiftSubprocessBuildURL, swiftSystemBuildURL, llbuildBuildURL, swiftArgumentParserBuildURL, swiftDriverBuildURL, swiftBuildBuildURL] {
try FileManager.default.createDirectory(at: url, withIntermediateDirectories: true)
}

Expand All @@ -75,6 +78,7 @@ struct CMakeSmokeTest: CommandPlugin {
"-DLLBuild_DIR=\(llbuildBuildURL.appending(components: "cmake", "modules").filePath)",
"-DTSC_DIR=\(swiftToolsSupportCoreBuildURL.appending(components: "cmake", "modules").filePath)",
"-DSwiftDriver_DIR=\(swiftDriverBuildURL.appending(components: "cmake", "modules").filePath)",
"-DSwiftSubprocess_DIR=\(swiftSubprocessBuildURL.appending(components: "cmake", "modules").filePath)",
"-DSwiftSystem_DIR=\(swiftSystemBuildURL.appending(components: "cmake", "modules").filePath)"
]

Expand All @@ -90,6 +94,11 @@ struct CMakeSmokeTest: CommandPlugin {
try await Process.checkNonZeroExit(url: ninjaURL, arguments: [], workingDirectory: swiftToolsSupportCoreBuildURL)
Diagnostics.progress("Built swift-tools-support-core")

Diagnostics.progress("Building swift-subprocess")
try await Process.checkNonZeroExit(url: cmakeURL, arguments: sharedCMakeArgs + [swiftSubprocessURL.filePath], workingDirectory: swiftSubprocessBuildURL)
try await Process.checkNonZeroExit(url: ninjaURL, arguments: [], workingDirectory: swiftSubprocessBuildURL)
Diagnostics.progress("Built swift-subprocess")

if hostOS != .macOS {
Diagnostics.progress("Building swift-system")
try await Process.checkNonZeroExit(url: cmakeURL, arguments: sharedCMakeArgs + [swiftSystemURL.filePath], workingDirectory: swiftSystemBuildURL)
Expand Down
5 changes: 0 additions & 5 deletions Sources/SWBCore/ProcessExecutionCache.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,6 @@ public final class ProcessExecutionCache: Sendable {
private let workingDirectory: Path?

public init(workingDirectory: Path? = .root) {
// FIXME: Work around lack of thread-safe working directory support in Foundation (Amazon Linux 2, OpenBSD). Executing processes in the current working directory is less deterministic, but all of the clients which use this class are generally not expected to be sensitive to the working directory anyways. This workaround can be removed once we drop support for Amazon Linux 2 and/or adopt swift-subprocess and/or Foundation.Process's working directory support is made thread safe.
if try! Process.hasUnsafeWorkingDirectorySupport {
self.workingDirectory = nil
return
}
self.workingDirectory = workingDirectory
}

Expand Down
2 changes: 1 addition & 1 deletion Sources/SWBTestSupport/Misc.swift
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ package func runProcessWithDeveloperDirectory(_ args: [String], workingDirectory
package func runHostProcess(_ args: [String], workingDirectory: Path? = nil, interruptible: Bool = true, redirectStderr: Bool = true) async throws -> String {
switch try ProcessInfo.processInfo.hostOperatingSystem() {
case .macOS:
return try await InstalledXcode.currentlySelected().xcrun(args, workingDirectory: workingDirectory, redirectStderr: redirectStderr)
return try await InstalledXcode.currentlySelected().xcrun(args, workingDirectory: workingDirectory, interruptible: interruptible, redirectStderr: redirectStderr)
default:
return try await runProcess(args, workingDirectory: workingDirectory, environment: .current, interruptible: interruptible, redirectStderr: redirectStderr)
}
Expand Down
5 changes: 0 additions & 5 deletions Sources/SWBTestSupport/SkippedTestSupport.swift
Original file line number Diff line number Diff line change
Expand Up @@ -152,11 +152,6 @@ extension Trait where Self == Testing.ConditionTrait {
})
}

/// Constructs a condition trait that causes a test to be disabled if the Foundation process spawning implementation is not thread-safe.
package static var requireThreadSafeWorkingDirectory: Self {
disabled(if: try Process.hasUnsafeWorkingDirectorySupport, "Foundation.Process working directory support is not thread-safe.")
}

/// Constructs a condition trait that causes a test to be disabled if the specified llbuild API version requirement is not met.
package static func requireLLBuild(apiVersion version: Int32) -> Self {
let llbuildVersion = llb_get_api_version()
Expand Down
4 changes: 2 additions & 2 deletions Sources/SWBTestSupport/Xcode.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,8 @@ package struct InstalledXcode: Sendable {
return try await Path(xcrun(["-f", tool] + toolchainArgs).trimmingCharacters(in: .whitespacesAndNewlines))
}

package func xcrun(_ args: [String], workingDirectory: Path? = nil, redirectStderr: Bool = true) async throws -> String {
return try await runProcessWithDeveloperDirectory(["/usr/bin/xcrun"] + args, workingDirectory: workingDirectory, overrideDeveloperDirectory: self.developerDirPath.str, redirectStderr: redirectStderr)
package func xcrun(_ args: [String], workingDirectory: Path? = nil, interruptible: Bool = true, redirectStderr: Bool = true) async throws -> String {
return try await runProcessWithDeveloperDirectory(["/usr/bin/xcrun"] + args, workingDirectory: workingDirectory, overrideDeveloperDirectory: self.developerDirPath.str, interruptible: interruptible, redirectStderr: redirectStderr)
}

package func productBuildVersion() throws -> ProductBuildVersion {
Expand Down
2 changes: 2 additions & 0 deletions Sources/SWBUtil/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ add_library(SWBUtil
POSIX.swift
Process+Async.swift
Process.swift
ProcessController.swift
ProcessInfo.swift
Promise.swift
PropertyList.swift
Expand Down Expand Up @@ -114,6 +115,7 @@ target_link_libraries(SWBUtil PUBLIC
SWBCSupport
SWBLibc
ArgumentParser
SwiftSubprocess::Subprocess
$<$<NOT:$<PLATFORM_ID:Darwin>>:SwiftSystem::SystemPackage>)

set_target_properties(SWBUtil PROPERTIES
Expand Down
3 changes: 2 additions & 1 deletion Sources/SWBUtil/Process+Async.swift
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ extension Process {
/// - note: This method sets the process's termination handler, if one is set.
/// - throws: ``CancellationError`` if the task was cancelled. Applies only when `interruptible` is true.
/// - throws: Rethrows the error from ``Process/run`` if the task could not be launched.
public func run(interruptible: Bool = true) async throws {
public func run(interruptible: Bool = true, onStarted: () -> () = { }) async throws {
@Sendable func cancelIfRunning() {
// Only send the termination signal if the process is already running.
// We might have created the termination monitoring continuation at this
Expand All @@ -115,6 +115,7 @@ extension Process {
}

try run()
onStarted()
} catch {
terminationHandler = nil

Expand Down
121 changes: 108 additions & 13 deletions Sources/SWBUtil/Process.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,18 +11,21 @@
//===----------------------------------------------------------------------===//

public import Foundation
import SWBLibc
public import SWBLibc
import Synchronization

#if os(Windows)
public typealias pid_t = Int32
#if canImport(Subprocess) && (!canImport(Darwin) || os(macOS))
import Subprocess
#endif

#if !canImport(Darwin)
extension ProcessInfo {
public var isMacCatalystApp: Bool {
false
}
}
#if canImport(System)
public import System
#else
public import SystemPackage
#endif

#if os(Windows)
public typealias pid_t = Int32
#endif

#if (!canImport(Foundation.NSTask) || targetEnvironment(macCatalyst)) && canImport(Darwin)
Expand Down Expand Up @@ -64,7 +67,7 @@ public typealias Process = Foundation.Process
#endif

extension Process {
public static var hasUnsafeWorkingDirectorySupport: Bool {
fileprivate static var hasUnsafeWorkingDirectorySupport: Bool {
get throws {
switch try ProcessInfo.processInfo.hostOperatingSystem() {
case .linux:
Expand All @@ -81,6 +84,30 @@ extension Process {

extension Process {
public static func getOutput(url: URL, arguments: [String], currentDirectoryURL: URL? = nil, environment: Environment? = nil, interruptible: Bool = true) async throws -> Processes.ExecutionResult {
#if canImport(Subprocess)
#if !canImport(Darwin) || os(macOS)
var platformOptions = PlatformOptions()
if interruptible {
platformOptions.teardownSequence = [.gracefulShutDown(allowedDurationToNextStep: .seconds(5))]
}
let configuration = try Subprocess.Configuration(
.path(FilePath(url.filePath.str)),
arguments: .init(arguments),
environment: environment.map { .custom(.init($0)) } ?? .inherit,
workingDirectory: (currentDirectoryURL?.filePath.str).map { FilePath($0) } ?? nil,
platformOptions: platformOptions
)
let result = try await Subprocess.run(configuration, body: { execution, inputWriter, outputReader, errorReader in
async let stdoutBytes = outputReader.collect().flatMap { $0.withUnsafeBytes(Array.init) }
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

two *Unsafe*s here

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

so? we can't use Span yet, this still has to build with 6.1

async let stderrBytes = errorReader.collect().flatMap { $0.withUnsafeBytes(Array.init) }
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

collect will load all of this potentially infinite stream into memory and I don't even see a limit, seems dangerous. If anything, limit it to something high like 32 MB and throw if we're blowing through that.

try await inputWriter.finish()
return try await (stdoutBytes, stderrBytes)
})
return Processes.ExecutionResult(exitStatus: .init(result.terminationStatus), stdout: Data(result.value.0), stderr: Data(result.value.1))
#else
throw StubError.error("Process spawning is unavailable")
#endif
#else
if #available(macOS 15, iOS 18, tvOS 18, watchOS 11, visionOS 2, *) {
let stdoutPipe = Pipe()
let stderrPipe = Pipe()
Expand Down Expand Up @@ -118,9 +145,40 @@ extension Process {
}
return Processes.ExecutionResult(exitStatus: exitStatus, stdout: Data(output.stdoutData), stderr: Data(output.stderrData))
}
#endif
}

public static func getMergedOutput(url: URL, arguments: [String], currentDirectoryURL: URL? = nil, environment: Environment? = nil, interruptible: Bool = true) async throws -> (exitStatus: Processes.ExitStatus, output: Data) {
#if canImport(Subprocess)
#if !canImport(Darwin) || os(macOS)
let (readEnd, writeEnd) = try FileDescriptor.pipe()
return try await readEnd.closeAfter {
// Direct both stdout and stderr to the same fd. Only set `closeAfterSpawningProcess` on one of the outputs so it isn't double-closed (similarly avoid using closeAfter for the same reason).
var platformOptions = PlatformOptions()
if interruptible {
platformOptions.teardownSequence = [.gracefulShutDown(allowedDurationToNextStep: .seconds(5))]
}
let configuration = try Subprocess.Configuration(
.path(FilePath(url.filePath.str)),
arguments: .init(arguments),
environment: environment.map { .custom(.init($0)) } ?? .inherit,
workingDirectory: (currentDirectoryURL?.filePath.str).map { FilePath($0) } ?? nil,
platformOptions: platformOptions
)
// FIXME: Use new API from https://github.com/swiftlang/swift-subprocess/pull/180
let result = try await Subprocess.run(configuration, output: .fileDescriptor(writeEnd, closeAfterSpawningProcess: true), error: .fileDescriptor(writeEnd, closeAfterSpawningProcess: false), body: { execution in
if #available(macOS 15, iOS 18, tvOS 18, watchOS 11, visionOS 2, *) {
try await Array(Data(DispatchFD(fileDescriptor: readEnd).dataStream().collect()))
} else {
try await Array(Data(DispatchFD(fileDescriptor: readEnd)._dataStream().collect()))
}
})
return (.init(result.terminationStatus), Data(result.value))
}
#else
throw StubError.error("Process spawning is unavailable")
#endif
#else
if #available(macOS 15, iOS 18, tvOS 18, watchOS 11, visionOS 2, *) {
let pipe = Pipe()

Expand Down Expand Up @@ -150,6 +208,7 @@ extension Process {
}
return (exitStatus: exitStatus, output: Data(output))
}
#endif
}

private static func _getOutput<T, U>(url: URL, arguments: [String], currentDirectoryURL: URL?, environment: Environment?, interruptible: Bool, setup: (Process) -> T, collect: @Sendable (T) async throws -> U) async throws -> (exitStatus: Processes.ExitStatus, output: U) {
Expand Down Expand Up @@ -215,9 +274,8 @@ public enum Processes: Sendable {
case exit(_ code: Int32)
case uncaughtSignal(_ signal: Int32)

public init?(rawValue: Int32) {
#if os(Windows)
let dwExitCode = DWORD(bitPattern: rawValue)
#if os(Windows)
public init(dwExitCode: DWORD) {
// Do the same thing as swift-corelibs-foundation (the input value is the GetExitCodeProcess return value)
if (dwExitCode & 0xF0000000) == 0x80000000 // HRESULT
|| (dwExitCode & 0xF0000000) == 0xC0000000 // NTSTATUS
Expand All @@ -227,6 +285,12 @@ public enum Processes: Sendable {
} else {
self = .exit(Int32(bitPattern: UInt32(dwExitCode)))
}
}
#endif

public init?(rawValue: Int32) {
#if os(Windows)
self = .init(dwExitCode: DWORD(bitPattern: rawValue))
#else
func WSTOPSIG(_ status: Int32) -> Int32 {
return status >> 8
Expand Down Expand Up @@ -306,6 +370,37 @@ public enum Processes: Sendable {
}
}

#if canImport(Subprocess) && (!canImport(Darwin) || os(macOS))
extension Processes.ExitStatus {
init(_ terminationStatus: TerminationStatus) {
switch terminationStatus {
case let .exited(code):
self = .exit(numericCast(code))
case let .unhandledException(code):
#if os(Windows)
// Currently swift-subprocess returns the original raw GetExitCodeProcess value as uncaughtSignal for all values other than zero.
// See also: https://github.com/swiftlang/swift-subprocess/issues/114
self = .init(dwExitCode: code)
#else
self = .uncaughtSignal(code)
#endif
}
}
}

extension [Subprocess.Environment.Key: String] {
fileprivate init(_ environment: Environment) {
self.init()
let sorted = environment.sorted { $0.key < $1.key }
for (key, value) in sorted {
if let typedKey = Subprocess.Environment.Key(rawValue: key.rawValue) {
self[typedKey] = value
}
}
}
}
#endif

extension Processes.ExitStatus {
public init(_ process: Process) throws {
assert(!process.isRunning)
Expand Down
Loading
Loading