CommonProcess provides a lean, testable, cross‑platform process execution core with a CommandSpec‑first model. Use it directly or embed it in your own shell/CLI layers.
Modern tools and CLIs depend on external processes. Doing this safely and
portably with ad‑hoc Process calls or stringly‑typed shells leads to drift,
flaky behavior, and poor observability. CommonProcess addresses this by
elevating process runs to a first‑class, codable spec with pluggable runners.
- Deterministic spec (CommandSpec)
- Identity + arguments + env + cwd + timeout + logging/instrumentation are captured in a single Codable value that can be logged, diffed, or replayed.
- Pluggable runners, portable defaults
- Select the best available backend per platform/toolchain
(
.subprocess,.tscbasic,.foundation), with graceful fallback.
- Select the best available backend per platform/toolchain
(
- Observability and policy
- Structured logging via
ProcessLogOptions; bounded previews; request IDs; optional instrumentation hooks for metrics and tracing.
- Structured logging via
- Safer composition than raw argv
- Executable identity (
.name,.path,.none) and argument composition rules eliminate many quoting/escaping pitfalls.
- Executable identity (
- Separation of concerns
- CommonProcess focuses on execution correctness; downstream layers can add wrappers (env/shell/npm/npx), ergonomics, and app‑level logging.
When to use
- Use CommonProcess when you need typed, replayable specs; runner selection; or you are building library code that must be portable and testable.
Tuist’s Command is a pragmatic, streaming wrapper over Foundation.Process and
works well inside the Tuist ecosystem. CommonProcess has a different goal and
scope:
- Typed, codable spec
- CommonProcess centers on
CommandSpecso runs are reproducible, and auditable across tools and CI. Tuist’sCommandis argv‑first and stringly‑typed.
- CommonProcess centers on
- Pluggable runners
- Choose between Subprocess/TSCBasic/Foundation (with
.autofallback) for better portability and capabilities. Tuist usesFoundation.Process.
- Choose between Subprocess/TSCBasic/Foundation (with
- Buffered and streaming
- Supports both buffered (
ProcessOutput) and streaming (AsyncSequence) flows with consistent preview/timeout policy. Tuist focuses on streaming.
- Supports both buffered (
- Observability
ProcessLogOptions, request IDs, and opt‑in instrumentation hooks are built in so you can bound previews and capture metrics by policy.
- Separation of concerns
- Host/wrapper policy (e.g., shell/env/npm/npx) lives above execution in downstream layers; CommonProcess stays focused on correctness.
If you only need a simple streaming call within Tuist, Tuist’s Command is a
good fit. If you want a typed, portable, instrumentable execution core that can
power multiple CLIs and libraries consistently, use CommonProcess.
- macOS 14+, iOS 17+, Mac Catalyst 17+
Targets:
CommonProcess- public core types (spec, output, logging, instrumentation)CommonProcessExecutionKit- factory + runner implementations (Foundation, swift-subprocess, TSCBasic)
Minimum platforms:
- macOS v14 (core + runners)
- iOS v17, macCatalyst v17 (core; Foundation runner unavailable on Catalyst)
This package ships libraries only:
CommonProcess(core types)CommonProcessExecutionKit(internal/backends + factory)
If you need a CLI for local experiments, host it in a separate package that depends on CommonProcess. Keep flags long-form and explicit.
- Foundation and WrkstrmFoundation is allowed in the library.
- For networking (Foundation and WrkstrmNetworking) portability, add the following import:
import Foundation
#if canImport(FoundationNetworking)
import FoundationNetworking
#endifAvoid UI frameworks in libraries (UIKit/AppKit). Keep UI in app targets.
- Aim for ≥80% line coverage for the library target.
- Keep tests deterministic and portable (macOS + Linux).
Backends policy
- Runner backends (Foundation/Subprocess/TSCBasic) are internal/private to the
CommonProcessExecutionKittarget. Direct uses ofFoundation.Processoccur only inside these internal implementations. Consumers interact viaRunnerControllerFactoryandProcessRunnerKind.
- List targets/products:
swift package describe
End-to-end examples using CommandSpec and the runner helpers.
Buffered run (capture stdout/stderr):
import CommonProcess
import CommonProcessExecutionKit
// 1) Choose an executable (by name or full path)
let exe: Executable = .name("echo")
// 2) Build a codable CommandSpec
var inv = CommandSpec(
executable: exe,
args: ["Hello, CommonProcess!"],
env: .inherit(updating: nil),
workingDirectory: nil,
logOptions: .init(exposure: .summary),
instrumentationKeys: [.metrics],
hostKind: .direct,
runnerKind: .auto,
timeout: .seconds(5)
)
// 3) Execute with the best available runner
let out = try await RunnerControllerFactory.run(command: inv)
guard out.exitStatus.isSuccess else {
throw ProcessError(status: out.exitStatus.exitCode, error: out.stderrText)
}
print(out.stdoutText)Streaming run (consume events as they arrive):
import CommonProcess
import CommonProcessExecutionKit
var inv = CommandSpec(
executable: .name("bash"),
args: ["-c", "for i in 1 2 3; do echo $i; sleep 0.1; done"],
logOptions: .init(
exposure: .summary,
maxStdoutLines: 10,
showHeadTail: true,
headLines: 5,
tailLines: 5
),
streamingMode: .passthrough
)
let (events, cancel) = RunnerControllerFactory.stream(command: inv)
Task {
for try await ev in events {
switch ev {
case .stdout(let d): print("OUT:", String(decoding: d, as: UTF8.self))
case .stderr(let d): print("ERR:", String(decoding: d, as: UTF8.self))
case .completed(let status, _): print("DONE:", status)
}
}
}
// Call cancel() to abort early if needed.Notes:
- On iOS/macCatalyst,
.subprocessand.tscBasicmay be unavailable;.autofalls back to a supported runner. - The Foundation runner encapsulates
Processon Apple platforms where supported (not on Mac Catalyst). - For ergonomic status checks/text decoding, see
ProcessExitStatusandProcessOutputconvenience APIs.
- Identity:
ExecutableReference(.name/.path/.none) +Executable(Codable, with default prefixes). - Run description:
CommandSpec(Codable) with fieldsexecutable,args,env,workingDirectory,logOptions,requestId,instrumentationKeys,hostKind, andrunnerKind. - Runner selection: prefer
runnerKindwhen supported; otherwise falls back to a platform default. - CLI support: tools may accept
CommandSpecJSON and surface flags like--instrumentation-keysand--runner-kindfor convenience.
- Built-in keys:
.noop(no-op) and.metrics(emits events viaProcessMetricsRecorder). ProcessLogOptions.tagscarries optional[String: String]labels that instrumentation or metrics recorders can forward.
swift-subprocess is a solid building block, but it only covers how to launch a child process.
CommonProcess goes further so teams get the same behavior across laptops and CI, with runs that
are reviewable and safe:
- Spec as data
CommandSpeccaptures executable identity, arguments, environment, working directory, timeouts, and expectations as one Codable value you can log, diff, and attach to PRs.
- Guardrails by default
- Timeouts, cancellation, and allowed exit codes reduce tail risks and make failure modes explicit instead of accidental.
- Streaming and buffered in one place
- Choose streaming for live feedback or buffered for post‑processing, with consistent preview bounds and redaction options.
- Cross‑platform runner selection
- Pick
.subprocess,.tscBasic, or.foundation(or.auto) per host without rewriting call sites; stay portable while using the best available backend.
- Pick
- First‑class observability
- Structured events and optional metrics hooks (request IDs, tags) make builds and incidents explainable. Specs serialize cleanly for audits.
Use Subprocess when you need a minimal spawn primitive; use CommonProcess when you want a typed, portable, observable execution core that scales to CI and multi‑tool workflows.
On GitHub Actions (Ubuntu), you can add a brief coverage summary using llvm-cov:
# 1) Ensure coverage artifacts exist
swift build --build-tests --enable-code-coverage
swift test --skip-build --enable-code-coverage --parallel
# 2) Install llvm-cov (version may vary on runners)
sudo apt-get update -y
sudo apt-get install -y llvm
# 3) Get paths and print a text report
PROF=$(swift test --show-codecov-path)
TEST_BIN=$(find .build -type f -name '*PackageTests*' | head -n 1)
llvm-cov report "$TEST_BIN" -instr-profile "$PROF"
Tips:
- If llvm-cov is provided under a different version, adjust the binary name (e.g.,
llvm-cov-15). - Keep CI fast: rely on caching
.buildwith zstd and split build/test as in the provided workflow.
- Cache
.buildwith actions/cache restore/save to reuse SwiftPM artifacts across runs.- Key includes OS and base SHA:
swiftpm-tests-build-${{ runner.os }}-${{ github.event.pull_request.base.sha || github.event.after }}. - Add Swift version to the key if you later use a matrix.
- Key includes OS and base SHA:
- Install
zstdbefore cache steps so actions/cache uses it automatically (faster than gzip/pigz). - Split build and tests to avoid rebuilding during test run:
- Build:
swift build --build-tests --enable-code-coverage(then save cache) - Test:
swift test --skip-build --enable-code-coverage --parallel
- Build:
- Run tests in parallel to cut wall-clock time; optionally cap workers with
--num-workers. - Keep archives and logs lean; publish a brief llvm-cov text summary when needed.
This package supports both public (remote) dependencies and local checkouts without hard‑coding any workspace‑specific paths.
- Default (public/standalone): resolves
WrkstrmLog,WrkstrmFoundation, andWrkstrmMainfrom GitHub remotes. - Local override (embedded workspace): set
SPM_USE_LOCAL_DEPS=truewhen invoking SwiftPM. The manifest will resolve Wrkstrm* dependencies from sibling directories relative to this package.
# Use local checkouts for Wrkstrm* libraries
SPM_USE_LOCAL_DEPS=true \
swift build -c releaseBehavior
- When
SPM_USE_LOCAL_DEPS=trueis present, the manifest uses local.package(name:path:)entries. - Otherwise, it falls back to public GitHub dependencies.
Why this design
- Aligns with WrkstrmMain/Foundation/Kit manifest pattern.
- Keeps the public manifest clean and buildable in isolation.
- Avoids leaking private repository structure in source while allowing fast local iteration.
Add to your Package.swift dependencies:
.package(url: "https://github.com/wrkstrm/common-process.git", from: "0.2.0")