Skip to content

wrkstrm/common-process

CommonProcess

Swift CI (Linux) License: MIT

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.

Why CommonProcess

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.
  • Observability and policy
    • Structured logging via ProcessLogOptions; bounded previews; request IDs; optional instrumentation hooks for metrics and tracing.
  • Safer composition than raw argv
    • Executable identity (.name, .path, .none) and argument composition rules eliminate many quoting/escaping pitfalls.
  • 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.

Why not just Tuist?

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 CommandSpec so runs are reproducible, and auditable across tools and CI. Tuist’s Command is argv‑first and stringly‑typed.
  • Pluggable runners
    • Choose between Subprocess/TSCBasic/Foundation (with .auto fallback) for better portability and capabilities. Tuist uses Foundation.Process.
  • Buffered and streaming
    • Supports both buffered (ProcessOutput) and streaming (AsyncSequence) flows with consistent preview/timeout policy. Tuist focuses on streaming.
  • 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.

Platforms

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

Choosing a mode

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.

Import policy

  • 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
#endif

Avoid UI frameworks in libraries (UIKit/AppKit). Keep UI in app targets.

Coverage

  • 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 CommonProcessExecutionKit target. Direct uses of Foundation.Process occur only inside these internal implementations. Consumers interact via RunnerControllerFactory and ProcessRunnerKind.

Commands reference

  • List targets/products:
    • swift package describe

Quickstart 🚀

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, .subprocess and .tscBasic may be unavailable; .auto falls back to a supported runner.
  • The Foundation runner encapsulates Process on Apple platforms where supported (not on Mac Catalyst).
  • For ergonomic status checks/text decoding, see ProcessExitStatus and ProcessOutput convenience APIs.

CommandSpec-first model (overview)

  • Identity: ExecutableReference (.name/.path/.none) + Executable (Codable, with default prefixes).
  • Run description: CommandSpec (Codable) with fields executable, args, env, workingDirectory, logOptions, requestId, instrumentationKeys, hostKind, and runnerKind.
  • Runner selection: prefer runnerKind when supported; otherwise falls back to a platform default.
  • CLI support: tools may accept CommandSpec JSON and surface flags like --instrumentation-keys and --runner-kind for convenience.

Instrumentation

  • Built-in keys: .noop (no-op) and .metrics (emits events via ProcessMetricsRecorder).
  • ProcessLogOptions.tags carries optional [String: String] labels that instrumentation or metrics recorders can forward.

Why more than just Subprocess

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
    • CommandSpec captures 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.
  • 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.

Linux CI coverage (optional)

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 .build with zstd and split build/test as in the provided workflow.

Faster CI rationale

  • Cache .build with 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.
  • Install zstd before 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
  • 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.

Dependency resolution (remote vs local)

This package supports both public (remote) dependencies and local checkouts without hard‑coding any workspace‑specific paths.

  • Default (public/standalone): resolves WrkstrmLog, WrkstrmFoundation, and WrkstrmMain from GitHub remotes.
  • Local override (embedded workspace): set SPM_USE_LOCAL_DEPS=true when 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 release

Behavior

  • When SPM_USE_LOCAL_DEPS=true is 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.

Install

Add to your Package.swift dependencies:

.package(url: "https://github.com/wrkstrm/common-process.git", from: "0.2.0")

About

🎛️ A codable cross platform command execution engine.

Resources

License

Code of conduct

Contributing

Security policy

Stars

Watchers

Forks

Packages

No packages published

Contributors 2

  •  
  •  

Languages