Skip to content

Releases: swiftlang/swift-subprocess

Subprocess 0.5

30 May 01:29
1163367

Choose a tag to compare

This release introduces major API changes to address feedback from Subprocess 1.0 Proposal pitch and review, in preparation for the 1.0 release. It is also the first release with Swift 6.1 Support Removed.

API Changes

Collapse run() overloads behind a generic Execution type (#262)

The 14+ run() overloads are reduced to two entry points (one Executable-based, one Configuration-based). Execution is now generic (Execution<Input, Output, Error>) and you opt into each interactive stream through the input: / output: / error: parameters. Type-conditional extensions then expose only the matching properties:

Pass this Unlocks on Execution
input: .inputWriter execution.standardInputWriter
output: .sequence execution.standardOutput
error: .sequence execution.standardError

Two companion changes:

  • Streams are now properties on execution, not extra closure parameters. The body closure always takes a single Execution value.
  • ExecutionOutcome is merged into ExecutionResult<ClosureResult, Output, Error>. Every run() now returns ExecutionResult; the closure's return value moves from ExecutionOutcome.value to ExecutionResult.closureOutput. (ExecutionOutcome survives only as an internal helper.) CustomWriteInput and SequenceOutput, plus the .inputWriter and .sequence factories, are now public.
// Before (0.4.x): the overload you chose implied which streams you got,
// and they arrived as extra closure parameters.
let outcome = try await run(
    .path("/usr/bin/tail"),
    arguments: ["-f", "/var/log/app.log"],
    input: .none,
    error: .discarded
) { execution, standardOutput in              // stream as a closure parameter
    for try await line in standardOutput.lines() { /* ... */ }
    return 42
}
print(outcome.value)                          // closure's return value

// After: opt in with `output: .sequence`, read from `execution.standardOutput`.
let result = try await run(
    .path("/usr/bin/tail"),
    arguments: ["-f", "/var/log/app.log"],
    input: .none,
    output: .sequence,                        // opt in to streaming stdout
    error: .discarded
) { execution in                              // single Execution value
    for try await line in execution.standardOutput.strings() { /* ... */ }
    return 42
}
print(result.closureOutput)                   // was `outcome.value`

Rename .standardOutput / .standardError output options to .currentStandardOutput / .currentStandardError (#233)

The output destinations that forward to the parent process's stdout/stderr were renamed to make that ownership explicit. (This is the output option; execution.standardOutput, the streaming property, keeps its name.) This PR also adopts NonisolatedNonsendingByDefault, so by default these async functions now run on the caller's executor rather than hopping to the global one.

// Before
try await run(.name("date"), output: .standardOutput, error: .standardError)
// After
try await run(.name("date"), output: .currentStandardOutput, error: .currentStandardError)

Rename AsyncBufferSequence to SubprocessOutputSequence (#274)

Renamed to avoid a clash with AsyncBufferSequence from swift-async-algorithms. Only affects code that spells the type out explicitly; inferred types in for try await loops need no change.

// Before
func handle(_ stream: AsyncBufferSequence) async throws { /* ... */ }
// After
func handle(_ stream: SubprocessOutputSequence) async throws { /* ... */ }

Rename LineSequence to StringSequence and add custom separators (#232)

AsyncBufferSequence.LineSequence becomes StringSequence, generalized to split on a custom Unicode-scalar separator (not just line breaks). .lines is reanmed to .strings; the new .strings(separatedBy:) covers custom delimiters, much like String.split.

// New: split on a custom separator.
for try await field in execution.standardOutput.strings(separatedBy: .unicodeScalarSequence([","])) {
    // each comma-separated field
}

read(upTo:) maxLength is now a cap; preferredBufferSize is removed (#259)

maxLength was treated as a target, accumulating in a loop until it was reached, which deadlocked when a child wrote fewer bytes and then waited. It's now a cap: each read returns as soon as the underlying nonblocking read(2)/ReadFile completes (accumulate yourself if you need an exact count). With the hang fixed, the preferredBufferSize: parameter which was added only as a workaround, is removed from every run() overload; buffer size is now derived from the pipe capacity. Resolves the deadlock in issue #252.

// Before
try await run(.name("cat"), output: .sequence, preferredBufferSize: 4096) { /* ... */ }
// After: drop the parameter
try await run(.name("cat"), output: .sequence) { /* ... */ }

Executable.resolveExecutablePath(in:) is now async (#264)

It hits the filesystem, so it can block. It's now async.

// Before
let path = try executable.resolveExecutablePath(in: .inherit)
// After
let path = try await executable.resolveExecutablePath(in: .inherit)

Unix teardown can target the whole process group (#243)

New sendTeardownSignalsToProcessGroup Boolean on Unix PlatformOptions. When true, every signal in the teardown sequence, including the implicit final SIGKILL, targets the subprocess's entire process group, so backgrounded grandchildren aren't orphaned.

var options = PlatformOptions()
options.sendTeardownSignalsToProcessGroup = true
options.teardownSequence = [.gracefulShutDown(allowedDurationToNextStep: .seconds(5))]

Windows toProcessGroup teardown (#260)

Extends #243 to Windows using Job Objects. Execution.terminate(withExitCode:) gains a toProcessGroup: parameter (default false); when true it calls TerminateJobObject, killing the child and all descendants — mirroring the Unix shape.

try execution.terminate(withExitCode: 1, toProcessGroup: true)  // child + all descendants

Behavioral note: every Windows child is now assigned to a Job Object at spawn, which requires CREATE_SUSPENDED to always be set (then resumed). If a preSpawnProcessConfigurator clears that flag, spawning now throws.

Important Bug Fixes

  • Cancel AsyncIO when the child process exits (#272). run() could hang when the child forked a grandchild that inherited the pipe FDs: the kernel kept the pipes open after the child exited, so the read/write loops never saw EOF/EPIPE. Process monitoring is now split into wait-for-termination and reap phases; a parallel monitor task races the body closure and cancels all pending reads/writes for the process the moment it exits (with PID-reuse safety). Resolves issue #256.
  • Fix process-monitoring hang on Linux kernels < 5.4 (#273). Installs a SIGCHLD handler via a new C shim, passes an empty sigmask to epoll_pwait so SIGCHLD is deliverable, and marks the self-pipe O_NONBLOCK. Also calls close_range through a direct syscall wrapper with fallback definitions, so it builds against glibc < 2.34 / older kernel headers (e.g. Amazon Linux 2, kernel 4.14). Resolves issue #271.
  • Graceful shutdown returns as soon as the subprocess exits (#281). Termination monitoring is raced against the teardown sequence in a task group: if the process exits first, the pending teardown step is cancelled and run() returns immediately instead of waiting out the grace period. waitForProcessTermination is now idempotent on all platforms.

Detailed Change Log

  • Remove Swift 6.1 support and update README by @iCharlesHu in #231
  • Adopt NonisolatedNonsendingByDefault and rename .standardOutput and .standardError output types to .currentStandardOutput and .currentStandardError by @iCharlesHu in #233
  • Rename AsyncBufferSequence.LineSequence to StringSequence and add custom Unicode scalar separator support by @iCharlesHu in #232
  • Update package name to swift-subprocess to match the rest of the ecosystem by @iCharlesHu in #236
  • Enable FreeBSD CI by @jakepetroules in #238
  • Configure Dependabot for GitHub Actions updates by @shahmishal in #237
  • Bump actions/checkout from 1 to 6 by @dependabot[bot] in #241
  • Bump the swiftlang-actions group with 2 updates by @dependabot[bot] in #240
  • Add a test for a custom OutputProtocol-conforming type by @itingliu in #235
  • Enable ExistentialAny, MemberImportVisibility, and InternalImportsByDefault by @jakepetroules in #242
  • Fix Windows test failures: use Write-Output and bypass execution policy by @jakepetroules in #255
  • Fix flaky testSubprocessDoesNotInheritRandomFileDescriptors on macOS CI by @jakepetroules in #257
  • Add the missing run() overload that allows body closure to stream both standard output and standard error while leaving input managed by @iCharlesHu in https://github.com/swiftlang/swift-subprocess/pull...
Read more

Subprocess 0.4

24 Mar 21:12
13d0876

Choose a tag to compare

Important

Subprocess 0.4 is the last planned release that supports Swift 6.1. We will remove 6.1 support in the next release. Please pin your dependency to 0.4 if you are still using Swift 6.1.

This release includes many important bug fixes and ships with several API changes:

API Changes

Error Overhaul (#220)

SubprocessError has been redesigned. Now you can compare SubprocessError.Code with a list of static values to check the failure reason. We also streamlined error throwing by ensuring Subprocess itself only throws SubprocessError while still allowing clients to throw any error in execution closures. With this redesign, we have the following recommendations on how to handle errors with Subprocess:

do {
    let result = try await run(
        .path(...),
    ) { execution, standardOutput in
        throw MyError()
        ...
        throw MyOtherError()
    }
} catch let subprocessError as SubprocessError {
    // Handle errors thrown by Subprocess itself
    // These errors usualy indicate something's wrong
    // with the environment or there's a bug in Subprocess itself
    switch subprocessError.code {
    case .spawnFailed:
        ...
    case .executableNotFound:
        ...
    }
} catch let myError as MyError {
    // Handle custom errors from the execution closure.
} catch let myOtherError as MyOtherError {
    // Handle custom errors from the execution closure.
}

Rename ExecutionResult and CollectedResult (#225)

ExecutionResult has been renamed to ExecutionOutcome and CollectedResult has been renamed to ExecutionRecord. The new names avoid confusion with Swift's Result type.

Deprecated type aliases for ExecutionResult and CollectedResult are provided to ease the transition. These aliases will be removed in the 1.0 release.

Update TerminationStatus for Windows (#229)

TerminationStatus has been updated on Windows to remove the .unhandledException case, which is a Unix-specific concept. Windows now uses only .exited, reflecting how Windows handles exit codes. On Unix, .unhandledException has been renamed to .signaled, as "exception" is not a standard Unix term.

Important Bug Fixes

Fix Compiler Warnings and Documentation (#224 #229)

All compiler warnings have been resolved, and documentation has been updated for correctness and clarity.

Update Windows Named Pipes with Unique Names (#230)

Fixed an issue on Windows where the named pipes used to communicate with child processes were not uniquely named, which could cause concurrently running Subprocess instances to crash.

Detailed Change Log

  • Subprocess error overhaul by @iCharlesHu in #220
  • Add nullability specifier to pthread_t pointer in process_shims.h by @rnapier in #221
  • Fix compiler warnings on all platforms by @iCharlesHu in #224
  • Rename ExecutionResult to ExecutionOutcome; rename CollectedResult to ExecutionRecord by @iCharlesHu in #225
  • Ensure Windows named pipes have unique names across processes. by @iCharlesHu in #230
  • Update documentation and TerminationStatus to match the 1.0 proposal by @iCharlesHu in #229

New Contributors

Full Changelog: 0.3...0.4

Subprocess 0.3

23 Jan 21:29
ba5888a

Choose a tag to compare

This release includes many important bug fixes. In particular:

#211 fixes an issue that causes swiftly to arbitrarily select swift executables in PATH.

What's Changed

New Contributors

Full Changelog: 0.2...0.3

Subprocess 0.2.1

21 Oct 21:28

Choose a tag to compare

This is a minor release that fixes two issues:

  • #201 Set interface include directory for the subprocess target
  • #199 Fix Environment.updating for custom and rawBytes

New Contributors

Full Changelog: 0.2...0.2.1

Subprocess 0.2

16 Oct 05:06
d781b8f

Choose a tag to compare

This release introduces two API changes and several important bug fixes.

API Changes

Introduced CombinedErrorOutput

A new output type for subprocesses that merges the standard error stream with the standard output stream.

When CombinedErrorOutput is used as the error output for a subprocess, both the standard output and standard error from the child process are combined into a single output stream. This is equivalent to using shell redirection such as 2>&1.

Updated Environment.updating to Accept [Key: String?]

This change allows an environment variable to be unset by assigning it a value of nil.

Important Bug Fixes

  • #196 Fixed CMake support. Subprocess can now be used as a dependent project in CMake builds.
  • #198 Fixed a hanging issue that occurs in release builds.

Detailed Change Log

New Contributors

Full Changelog: 0.1...0.2

Subprocess 0.1

05 Sep 03:53
44be5d5

Choose a tag to compare

This is the initial release of swift-subprocess.

API Changes Since Initial Proposal

  • Introduce preferredBufferSize parameter to allow custom buffer size when streaming subprocess output (#168)

    A new preferredBufferSize parameter was added to the streaming APIs, allowing developers to control the buffer size when reading subprocess output instead of relying on a fixed default.

public func run<Result, Input: InputProtocol, Error: OutputProtocol>(
    _ executable: Executable,
    arguments: Arguments = [],
    environment: Environment = .inherit,
    workingDirectory: FilePath? = nil,
    platformOptions: PlatformOptions = PlatformOptions(),
    input: Input = .none,
    error: Error = .discarded,
    preferredBufferSize: Int? = nil, // New API
    isolation: isolated (any Actor)? = #isolation,
    body: ((Execution, AsyncBufferSequence) async throws -> Result)
) async throws -> ExecutionResult<Result> where Error.OutputType == Void
  • Expand API with Configuration-based run() overloads (#164)

    Added new run() overloads that allow subprocesses to be launched using a Configuration, bringing parity with APIs that take individual parameters.

// New Configuration-based APIs

public func run<
    InputElement: BitwiseCopyable,
    Output: OutputProtocol,
    Error: OutputProtocol
>(
    _ configuration: Configuration,
    input: borrowing Span<InputElement>,
    output: Output,
    error: Error = .discarded
) async throws -> CollectedResult<Output, Error>

public func run<Result, Input: InputProtocol, Output: OutputProtocol, Error: OutputProtocol>(
    _ configuration: Configuration,
    input: Input = .none,
    output: Output = .discarded,
    error: Error = .discarded,
    isolation: isolated (any Actor)? = #isolation,
    body: ((Execution) async throws -> Result)
) async throws -> ExecutionResult<Result> where Error.OutputType == Void

public func run<Result, Input: InputProtocol, Error: OutputProtocol>(
    _ configuration: Configuration,
    input: Input = .none,
    error: Error = .discarded,
    isolation: isolated (any Actor)? = #isolation,
    body: ((Execution, AsyncBufferSequence) async throws -> Result)
) async throws -> ExecutionResult<Result> where Error.OutputType == Void

public func run<Result, Input: InputProtocol, Output: OutputProtocol>(
    _ configuration: Configuration,
    input: Input = .none,
    output: Output,
    isolation: isolated (any Actor)? = #isolation,
    body: ((Execution, AsyncBufferSequence) async throws -> Result)
) async throws -> ExecutionResult<Result> where Output.OutputType == Void

public func run<Result, Error: OutputProtocol>(
    _ configuration: Configuration,
    error: Error = .discarded,
    isolation: isolated (any Actor)? = #isolation,
    body: ((Execution, StandardInputWriter, AsyncBufferSequence) async throws -> Result)
) async throws -> ExecutionResult<Result> where Error.OutputType == Void

public func run<Result, Output: OutputProtocol>(
    _ configuration: Configuration,
    output: Output,
    isolation: isolated (any Actor)? = #isolation,
    body: ((Execution, StandardInputWriter, AsyncBufferSequence) async throws -> Result)
) async throws -> ExecutionResult<Result> where Output.OutputType == Void
  • Remove preSpawnProcessConfigurator on Linux

    This property has been removed due to async-signal-safety concerns. It is not possible to offer a safe implementation of this API.

  • Remove the default collected output buffer limit and throw an error when the limit is reached (#130)

    The default output buffer has been removed. All run() methods now require developers to explicitly specify the output type and buffer limit. If the limit is reached, an error will be thrown. This change ensures developers choose buffer sizes that best fit their use case.

@@ -9,6 +9,6 @@ public func run<
     workingDirectory: FilePath? = nil,
     platformOptions: PlatformOptions = PlatformOptions(),
     input: Input = .none,
-    output: Output = .string,
+    output: Output,
     error: Error = .discarded
 ) async throws -> CollectedResult<Output, Error>
  • Expose platform-specific process file descriptors (#101)

    Linux, FreeBSD, and Windows now expose their respective process descriptors (pidfd on Linux and FreeBSD, HANDLE on Windows). These allow integration with system APIs that require descriptors rather than raw process IDs.

@@ -2,7 +2,7 @@
 public struct ProcessIdentifier: Sendable, Hashable {
     /// The platform-specific process identifier value
     public let value: pid_t
-    internal let processDescriptor: PlatformFileDescriptor
+    public let processDescriptor: PlatformFileDescriptor
 }
 #endif
 
@@ -10,7 +10,7 @@ public struct ProcessIdentifier: Sendable, Hashable {
 public struct ProcessIdentifier: Sendable, Hashable {
     /// Windows-specific process identifier value
     public let value: DWORD
-    internal nonisolated(unsafe) let processDescriptor: HANDLE
-    internal nonisolated(unsafe) let threadHandle: HANDLE
+    public nonisolated(unsafe) let processDescriptor: HANDLE
+    public nonisolated(unsafe) let threadHandle: HANDLE
 }
 #endif
  • Remove runDetached API (#95)

    runDetached was originally proposed as the synchronous counterpart to run(). However, since posix_spawn can block, this API could not be implemented synchronously. It has therefore been removed.

  • Make Configuration.workingDirectory optional (#74)

    Configuration.workingDirectory is now optional. A nil value means the subprocess will inherit the parent process’s working directory.

@@ -2,6 +2,6 @@ public struct Configuration: Sendable {
     public var executable: Executable
     public var arguments: Arguments
     public var environment: Environment
-    public var workingDirectory: FilePath
+    public var workingDirectory: FilePath?
     public var platformOptions: PlatformOptions
 }
  • AsyncBufferSequence.Buffer Improvements (#48)

    Introduced AsyncBufferSequence.LineSequence as the preferred way to stream output as text. This converts the raw Buffer sequence into an asynchronous sequence of lines.

extension AsyncBufferSequence {
    public struct LineSequence<Encoding: _UnicodeEncoding>: AsyncSequence, Sendable {
        public typealias Element = String

        public enum BufferingPolicy: Sendable {
           case unbounded
           case maxLineLength(Int)
       }

        public struct AsyncIterator: AsyncIteratorProtocol {
            public mutating func next() async throws -> String?
        }

        public func makeAsyncIterator() -> AsyncIterator
    }

    public func lines() -> LineSequence<UTF8>

    public func lines<Encoding: _UnicodeEncoding>(
        encoding: Encoding.Type,
        bufferingPolicy: LineSequence<Encoding>.BufferingPolicy = .maxLineLength(128 * 1024)
    ) -> LineSequence<Encoding>
}
  • Make Environment keys case-insensitive on Windows (#174)

    Introduced Environment.Key to provide a platform-aware way of accessing environment variables. Keys are now case-insensitive on Windows, while retaining case sensitivity on platforms where that is the convention.

public struct Key: ExpressibleByStringLiteral, Codable, Hashable, RawRepresentable, CustomStringConvertible, Sendable{
    public var rawValue: String

    public init(stringLiteral rawValue: String)
}