Update dependency swiftlang/swift-subprocess to v1 - #152
Open
renovate[bot] wants to merge 1 commit into
Open
Conversation
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This PR contains the following updates:
from: "0.2.0"→from: "1.0.0"Release Notes
swiftlang/swift-subprocess (swiftlang/swift-subprocess)
v1.0.0: Subprocess 1.0.0Compare Source
Subprocess 1.0 is here. 🎉 This release marks an important milestone for this package: the advent of source stability!
Subprocess is a cross-platform package for spawning processes in Swift, supporting macOS, Linux, Windows, FreeBSD, OpenBSD, and Android. It was first pitched as SF-0007 and shipped as a public beta in spring 2025. A year of community feedback later, SF-0037 reviewed the accumulated API changes, and this release makes them final.
This note has two parts. If you last looked at Subprocess when it went through Swift Evolution, start with What Changed Since SF-0007. If you're already on
1.0.0-beta.1, skip to What Changed Since 1.0.0-beta.1.What Changed Since SF-0007
A single
run()closure form, and a unifiedExecutionResultSF-0007 handled standard input out of step with the other two streams. Reading output meant iterating
execution.standardOutput, but writing input meant reaching for a separate family ofrun()overloads whose closure took an extraStandardInputWriterargument. Every combination of "writes to standard input or not" needed its own overload, which produced a combinatorial explosion.Executionis now generic over itsInputtype as well, so all three streams work the same way. Type-conditional extensions exposestandardInputWriter,standardOutput, andstandardErroronly when the matching stream is redirected:CustomWriteInputandSequenceOutputwere previously internal; they and their.inputWriterand.sequencefactories are now public. You opt into each stream independently:input: .inputWritergives youexecution.standardInputWriter,output: .sequencegives youexecution.standardOutput, anderror: .sequencegives youexecution.standardError.Because SF-0007 already exposed
standardOutputandstandardErrorconditionally, this leaves most call sites untouched since the visible change is concentrated on standard input. A call site that only reads output and error needs no changes at all.The two result types are unified too.
CollectedResult<Output, Error>is gone, and everything now flows through a single genericExecutionResult:ClosureResultisVoidfor the collectedrun()overloads and the closure's return type otherwise. Beyond collapsing the overload set, this unlocks something that was previously impossible: collecting and streaming at the same time, since you're no longer forced to choose between two separate result types.The closure-based overloads require explicit
input:,output:, anderror:arguments. They have no defaults, so the compiler can determine which streaming properties theExecutionvalue exposes. The collected overloads keep the familiarinput: .noneanderror: .discardeddefaults.Two smaller changes round this out. Body closures may now return noncopyable values: the
Resulttype parameter is~Copyable,ExecutionResultisCopyableexactly when itsClosureResultis, and you move a move-only value out with the consumingtakeClosureResult().And Subprocess now adopts the
NonisolatedNonsendingByDefaultupcoming feature, which let us drop theisolation: isolated (any Actor)? = #isolationparameter from every closure-basedrun()overload.Streaming output:
SubprocessOutputSequenceandStringSequenceExecution.standardOutputandstandardErrorused to return an opaquesome AsyncSequence<Buffer, any Swift.Error>. They now return a concrete, publicSubprocessOutputSequence, with the element type re-nested asSubprocessOutputSequence.Buffer.SubprocessOutputSequenceowns the underlying OS pipe, so it is single-pass: callingmakeAsyncIterator()more than once traps. The read buffer size is derived automatically from the platform's pipe buffer size.Bufferremains an immutable byte collection whose primary accessor is aRawSpan, and with theSubprocessFoundationtrait enabled,Datagains aninit(buffer:)that copies from one.Streaming text is one of the most common things people do with Subprocess, and it was awkward before: naively converting each
Bufferto aStringbreaks whenever a buffer boundary splits a multi-byte character.SubprocessOutputSequence.StringSequencehandles the reassembly for you.You can create a
StringSequenceby calling.strings(separatedBy:bufferingPolicy:)onSubprocessOutputSequence. By default it splits on Unicode line breaks (LF, VT, FF, CR, CR+LF, NEL, LS, and PS), with separators excluded from the returned strings the way.split(separator:)behaves. You can supply your own delimiter with.unicodeScalarSequence(_:). Note that it matches at the code-unit level without Unicode normalization, so a precomposed "é" (U+00E9) won't match a decomposed one (U+0065 U+0301). You can also pick aStringencoding, and control back-pressure with aBufferingPolicyof either.unboundedor.maxLineLength(_:)(the default is 128 KB; exceeding it throws).StringOutput.OutputTypeis now non-optional, and all output limits are explicitStringOutput.OutputTypewasString?in SF-0007, on the theory that decoding raw bytes might fail. In practice the implementation usedString(decoding:as:), which always succeeds by substituting the Unicode replacement character (U+FFFD), so the optional was nevernil, and everyone paid an unwrap for a failure that couldn't happen.OutputTypeis now a non-optionalString. You can still detect U+FFFD if you care about invalid input.Relatedly, output factories now require an explicit limit. The zero-argument
.string,.bytes, and.dataconveniences silently capped collection at 128 KB; they're replaced by.string(limit:),.string(limit:encoding:),.bytes(limit:), and.data(limit:). Subprocess throwsoutputLimitExceededwhen a process produces more than the limit. Because the default.stringoutput is gone, the collectedrun()overloads now require an explicitoutput:argument. This design makes the maximum memory arun()call may allocate visible at the call site.Error overhaul
SubprocessErrorhad two problems: itsCodewas an opaqueIntthat you had to memorize, and the library never formalized what it throws or how you should catch it.SubprocessError.Codeis now a proper type with named static properties:.spawnFailed,.executableNotFound,.failedToChangeWorkingDirectory,.failedToMonitorProcess,.failedToReadFromSubprocess,.failedToWriteToSubprocess,.outputLimitExceeded,.asyncIOFailed, and.processControlFailed.On Windows,
WindowsErroris no longer a thin wrapper around a singleGetLastError()DWORD. Windows surfaces errors through several distinct subsystems, so it's now anenumwith.ntStatus,.win32,.hresult, and.cRuntimecases.The throwing contract is formalized as well: Subprocess itself only ever throws
SubprocessErrorsince most internals now use typed throws. The only other errors you'll see are the ones you throw yourself from a body closure or from.preSpawnProcessConfigurator. That gives error handling a clean shape:Environment.KeyEnvironment keys are case-insensitive on Windows and case-sensitive everywhere else. Raw
Stringkeys papered over that difference whereas a dedicatedEnvironment.Keytype respects each platform's rules. It'sExpressibleByStringLiteral, so literals keep working unchanged.The
Environmentmethods that took[String: String]now take[Key: ...].updating(_:)additionally acceptsnilvalues, so you can remove an inherited variable before it reaches the child:Combining standard error into standard output
Merging the two streams the way
2>&1does is a common enough request that it now has a first-class spelling:.combinedWithOutput.This required expanding the protocol hierarchy. Every other output type works for either stream, but
CombinedErrorOutputonly makes sense for standard error. So there's now anErrorOutputProtocolthat refinesOutputProtocoland adds no new requirements, and theerror:parameter ofrun()is constrained to it. All the built-in output types conform to both, so they still work for either stream, onlyCombinedErrorOutputis error-only.Redirecting to the parent's streams
FileDescriptorOutputgains.currentStandardOutputand.currentStandardError, which forward the child's output to the parent's own streams. This feature is useful when you want to follow along with a process rather than capture it. Symmetrically,FileDescriptorInputgains.currentStandardInput, which feeds the child the parent's standard input. None of these close the underlying descriptor afterward.Process identity, termination, and the removal of
runDetachedrunDetached()is removed. It was pitched as an escape hatch for spawning synchronously where concurrency might be unavailable. It was designed to be a thin wrapper overposix_spawnthat returned a child PID and did no async I/O or state monitoring. The problem is PID reuse. On Windows a PID has no concept ofwait()and reaping, and can be recycled the instant the process terminates, so the PID may already be invalid by the timerunDetached()returns. Rather than build an elaborate workaround for a TOCTOU race in an API that was never core to Subprocess, we removed it.To address that same PID-reuse hazard in the API that remains,
ProcessIdentifiernow exposes platform-specific process descriptors. On Linux, Android, and FreeBSD it carries aprocessDescriptor: CInt(apidfdon Linux) alongsidevalue: pid_t; on Windows it carries aprocessDescriptor: HANDLEand athreadHandle: HANDLE. Darwin continues to wrap just thepid_t. We recommend using the descriptor rather than the raw PID. Per the Linux documentation, even if the child has already terminated by the time of thepidfd_open()call, its PID will not have been recycled and the descriptor refers to the resulting zombie.TerminationStatusis redesigned for Windows. Its.exited()/.unhandledException()split reflected Unix'swait(2)bitfield, which distinguishes normal exits from signals. Windows'sGetExitCodeProcess()returns a singleDWORD, so that distinction can't be reconstructed..unhandledException()is therefore removed on Windows, and renamed to.signaled()on Unix, where signal delivery is what actually happened.Both
ProcessIdentifierandTerminationStatusare nowSendable, Hashable; their SF-0007Codableconformances are removed, since process descriptors are process-local and not meaningfully serializable.Teardown
TeardownStep.sendSignal(_:allowedDurationToNextStep:)is renamed to.send(signal:toProcessGroup:allowedDurationToNextStep:), and.gracefulShutDown(...)gains the sametoProcessGroupparameter (and a fix for the misspelledalloweDurationToNextSteplabel). Targeting the process group means descendants don't leak after teardown, and the implicit final.killstep inheritstoProcessGroupfrom the last explicit step.PlatformOptionsOn all platforms,
PlatformOptionsno longer conforms toHashable. It's nowSendableplusCustomStringConvertible/CustomDebugStringConvertible. The closure-valued escape-hatch properties never had a meaningfulHashableimplementation, so the conformance was misleading.On Darwin,
launchRequirementDatais removed; it was never wired up to a supported launch path.On Linux and other non-Darwin Unix platforms, the
preSpawnProcessConfiguratorescape hatch is removed. It runs betweenforkandexec, where only async-signal-safe work is permitted, and we can't offer that safely as a public API. It remains available on Darwin (operating onposix_spawnattr_t/posix_spawn_file_actions_t) and on Windows (operating ondwCreationFlags/STARTUPINFOW). The non-Darwin Unix options are now:On Windows,
UserCredentialsand theuserCredentialsproperty areinternalfor 1.0 while their behavior is finalized, and the misspelledConsoleBehavior.detatchis corrected to.detach.Swift 6.2 is now required
Subprocess was designed around
Spanas the currency type for file I/O, but we wanted Swift 6.1 to work at beta time so more people could try it. That meant shims and workarounds behind aSubprocessSpantrait. Swift 6.2 has been out for over a year, so 1.0 drops the workarounds: the package requiresswift-tools-version: 6.2.If you need Swift 6.1, use the
0.4tag as it's the final version of Subprocess that supports it.This removes the
SubprocessSpantrait and theSequence<UInt8>-based fallback onOutputProtocol, leavingRawSpanas the single currency type.OutputProtocolandInputProtocolalso gain a~Copyablerelaxation, so noncopyable types can conform.Other refinements
Configurationis no longerHashable/Equatable. It's nowSendableplusCustomStringConvertible/CustomDebugStringConvertible, consistent withPlatformOptions. Its initializer label changes frominit(executing:)toinit(executable:), andworkingDirectorybecomes anOptional<FilePath>stored property, wherenilinherits the parent's working directory.Executable.resolveExecutablePath(in:)is nowasyncand uses typed throws since resolving a path may touch the filesystem on a background thread.StandardInputWriterwrite methods adopt typed throws (throws(SubprocessError)), and theRawSpanoverload is now unconditionally available rather than gated on the removedSubprocessSpantrait.What Changed Since 1.0.0-beta.1
API Changes
Executable.name(_:)searchesPATHand nothing else, on every platform (#357)Executable.name(_:)is documented as aPATHlookup, but the resolver also searched a current directory, ahead ofPATH, and the details differed between the eagerresolveExecutablePath(in:)and the spawn path, and between Unix and Windows. On Unix a bare./toolbeat everyPATHentry. This is the classic dot-in-PATHhazard, which turns "clone this repo and run the tool" into arbitrary code execution when a checkout contains a file namedgit,swift, ormake. On Windows the search wasCreateProcessW's, which covers the application directory, the current directory, and the system directories beforePATH, and which readsPATHfrom the calling process rather than from the environment you pass to the subprocess..name(_:)now means one thing everywhere: walk the directories listed inPATH, in order, and run the first match.PATHsearched is the subprocess's. The value from the environment you pass torunwins, so a name resolves in the environment it will run in. When that environment sets noPATH, the current process's value is used. Windows no longer delegates the search toCreateProcessW, which would otherwise ignore thePATHinlpEnvironment, matching what Node.js and Rust do, rather than shipping the divergence as Go and Python do.workingDirectoryyou pass torun. EmptyPATHentries (from a leading, trailing, or doubled separator) and relative entries are skipped, since both are a current-directory search by another name. Every resolved path is therefore absolute, andresolveExecutablePath(in:)and the spawn path agree on which executable a configuration names.PATHexists. A helper executable shipped next to your app is no longer found by name; name it withpath(_:)instead.SubprocessErrorwhose code is.spawnFailed, rather than being resolved against a current directory./counts on every platform;\and:also count on Windows.The
PATH-less fallback now asks the system for the defaultPATHinstead of using a hard-coded list. When neither the subprocess environment nor the current process definesPATHat all, Unix-like platforms perform the following fallbackPATHresolution: 1) First, searchconfstr(_CS_PATH), which is the same standard pathexecvp(3)uses and whatgetconf PATHprints; 2) then, fall back to the<paths.h>macro; 3) finally, fall back to a hard-coded list (/usr/bin:/bin:/usr/sbin:/sbin:/usr/local/bin).Windows searches the directories
CreateProcessWsearches on its own: the application directory, the 32-bit and 16-bit system directories, and the Windows directory, with the current directory left out. Two Windows-only improvements fall out of resolving the name ourselves.resolveExecutablePath(in:)now appliesPATHEXT, so.name("cmd")resolves rather than requiring.name("cmd.exe"); and aPATHEXTextension is appended rather than substituted, so.name("python3.11")looks forpython3.11and thenpython3.11.exeinstead ofpython3.exe. A name that resolves to a.bator.cmdruns through the hardenedcmd.exeinvocation added for CVE-2024-24576, so.name("npm")findingnpm.cmdis safe.StringOutput.OutputTypeis now a non-optionalString(#338)StringOutput.output(from:)never actually returnednil. It decodes throughString(decoding:as:), which always succeeds by substituting U+FFFD for invalid byte sequences. The optionalOutputTypetherefore imposed an unwrap for a case that could not occur. It's nowString, which also makesStringOutputconsistent withDataOutput, whoseOutputTypewas already a non-optionalData.Invalid bytes still become U+FFFD, so you can check for the replacement character if your input may not be well-formed text.
Deprecated
FileDescriptorOutputaliases removed (#349).standardOutputand.standardErrorwere reintroduced in beta.1 as@available(*, deprecated, renamed:)aliases to ease the transition. They're now removed for 1.0.InputProtocol.standardInputrenamed to.currentStandardInput(#356)The input side was missed when the output properties were renamed to the
current*spelling. It's renamed now so all three parent-stream redirections read the same way.Bug Fixes
setgid()beforesetuid()when spawning (#344). Setting bothuserIDandgroupIDinPlatformOptionsfailed to spawn withEPERM. Oncesetuid()drops the effective UID out of the superuser, the kernel clears the permitted capability set on Linux and the saved-set-ID rules bite on Darwin and the BSDs, so the subsequentsetgid()was no longer permitted. The calls are reordered in both spawn paths. Resolves #342.pthread_cond_waitis permitted to return without a matching signal, and does so on Linux when the waiting thread is interrupted by a signal. The wait now loops on the predicate instead of branching once, so a spurious wakeup re-checks and goes back to sleep rather than letting the worker exit early and hang callers with leaked continuations. A shutdown flag keeps the worker able to wake and exit. Resolves #348.issueAndAwaitRead()andwrite(_:to:for:)rethrew with an overlapped operation still pending against the caller's buffer, which the kernel could then write into (or read from) after the frame unwound. Bothcatchbranches now route throughsettlePendingOverlapped(), cancelling withCancelIoEx()and waiting withGetOverlappedResult()before the buffer goes out of scope.Documentation & Infrastructure
run()and its more common inputs and patterns, including collecting results, searching for executables versus supplying a path, and stream processing. by @heckj in #347SubprocessOutputSequenceprecondition that creating a second iterator is a fatal error. by @broken-circle in #352swift-docc-plugindependency, which unbreaks documentation builds in swiftlang/docs and other tooling. by @ktoso in #354swiftlang/github-workflowssoundness workflow to 0.0.12 and 0.0.13. by @dependabot in #341, #355Detailed Change List
setgid()beforesetuid()in the spawned subprocess by @broken-circle in #344StringOutput.OutputTypeto non-optional by @broken-circle in #338FileDescriptorOutputaliases by @broken-circle in #349SubprocessOutputSequenceprecondition by @broken-circle in #352InputProtocol.standardInputtocurrentStandardInputby @iCharlesHu in #356Full Changelog: swiftlang/swift-subprocess@1.0.0-beta.1...1.0.0
Configuration
📅 Schedule: (UTC)
🚦 Automerge: Disabled by config. Please merge this manually once you are satisfied.
♻ Rebasing: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.
🔕 Ignore: Close this PR and you won't be reminded about this update again.
This PR was generated by Mend Renovate. View the repository job log.