Announcing swift-subprocess 0.1

Hello everyone,

I'm happy to announce that we've just released the first version of swift-subprocess, version 0.1.

This release already includes a number of API refinements since the package was made public on GitHub. Below is a summary of the key changes.

  • 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
+    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
+    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)
}

As initially pitched, our current plan is to start a beta period for swift-subprocess. During this period, we'll actively seek and fix bug reports from the community for several months before we proceed with tagging the 1.0 release.

Our goal is to conclude the beta period and release the first official version in the first week of January 2026.

Please give the package a try! If you encounter bugs or have suggestions, open an issue on the GitHub repository.

Thank you,

Charles

21 Likes