Execra
DO NOT DEPEND ON THIS CRATE.
Execra is currently in heavy testing inside my own rScoop project as the load-bearing case for whether it works as a general-purpose public crate. Depending on it externally right now is not supported: the API, the wire format, and the persistence schema will change without notice, and the crate may end up being narrowed back into an rScoop-only implementation detail rather than a published library.
The repository is public so the design can be reviewed and so rScoop can consume it from crates.io while it stabilises. If and when that happens, this notice will be removed and a
1.0will set the stability contract. Until then: read, fork, learn from it — do notcargo addit in production.
Execra is a typed job runtime for external processes in Rust. It spawns a child process and returns a job handle that can be awaited for its final outcome or subscribed to as a stream of structured events: phase changes, progress updates, findings, warnings, known errors, and terminal state.
The runtime owns process lifecycle, output decoding, process-group
cancellation, timeout enforcement, and persistence. An optional
Interpreter turns each CLI's output lines into typed events, so the
runtime stays shell-agnostic and every consumer — a CLI, a desktop UI, a
language binding — reads the same event shape.
This crate is the core runtime. A execra-cli and execra-tauri adapter
live in the workspace and consume the same library.
Status
0.1.x, not for external consumption (see the notice at the top of
this README). The wire format is v0 and still subject to change. See
SCHEMA.md for the versioning policy.
Usage
Add the crate to your Cargo.toml:
[]
= "0.1"
Open the runtime once at application startup (typically held in shared
state), spawn jobs against it, and either await the handle or subscribe to
events. The same spawn call serves both headless and UI callers.
use ;
async
To drive a UI, keep the handle, subscribe, and forward events:
# use ;
# async
Cancellation is per-job and kills the entire process group, so spawned
children (for example aria2 invoked by scoop) terminate too:
# use ;
#
Feature flags
bundled-sqlite(default) — Build SQLite intorusqlite. Disable to link against the systemlibsqlite3.gzip(default) — EnableRawOutputPolicy::PersistGzipOnFinalizeand pull inflate2. Raw logs can still be persisted uncompressed without this feature.
Both default features are additive; disabling either does not change the event wire format or the public API surface aside from the variant noted above.
Concepts
The crate is built around a small number of types. Full definitions are in
SCHEMA.md; the summary below is enough to follow the API.
Job — one invocation of a Command, plus the runtime's view of it
(state, current phase, progress, exit code, outcome).
Event — the wire protocol. JobCreated, PhaseEntered,
ProgressUpdated, OutputAppended, FindingEmitted, Exited,
Finalized, and so on. Events are append-only and totally ordered per
job. Every consumer reads the same enum.
Progress — Unknown, Indeterminate { hint }, or
Determinate(ProgressMetric) where ProgressMetric is a fraction, an
item count, or a byte count. Interpreters emit raw units; UIs format.
Finding — a structured observation (Severity, stable code,
message, optional typed Action) that survives finalization in
Outcome.findings. This is how scoop doctor, linters, audits, and
dry-runs surface results regardless of exit code.
Outcome — Succeeded { findings }, Failed { reason, findings }, or
Cancelled { findings }. Exit code owns the verdict; interpreters enrich
but do not override.
Interpreter — the trait that turns output lines into the events
above. One interpreter per job. See INTERPRETER.md
and the examples/ for worked references.
Runtime behaviour
Command::newis shell-agnostic. For convenience,Command::shell,Command::cmd,Command::sh,Command::powershell, andCommand::pwshwrap the program in the appropriate platform shell.- Output bytes are decoded with lossy UTF-8 so invalid byte sequences do not drop lines.
- Cancellation kills the process group:
setpgid+kill(-pgid, ...)on Unix, a Win32 Job Object withJOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSEon Windows. ExitedandFinalizedare distinct events.Exitedcarries the raw OS exit code;Finalizedcarries the interpretedOutcome. Both fire exactly once for a job that started.- Two persistence stores back the runtime: SQLite for jobs, findings, and interpreted events; flat files (optionally gzipped on finalization) for raw output. This keeps the database small even for chatty CLIs.
Configexposes the database path, log directory, retention policy, concurrency cap, and grace period for cancellation. Per-job timeouts go on theCommand.
Non-goals
Execra is deliberately not these things:
- A shell parser. Pass
program + args, or use a shell helper explicitly. - A way to run in-process work. Filesystem operations, pure-Rust transformations, and anything else that is not a child process should stay in plain Rust.
- A DAG / composition engine. Callers chain jobs with
.await. - A replacement for
cargo/git/npmunderstanding. The runtime does not pretend to know what a CLI means without an interpreter; it makes interpreters cheap to write instead. - A way to hide the underlying process. Raw output is always available
via
OutputAppendedevents; interpretation is additive, never a replacement.
Repository layout
SCHEMA.md— Types, events, terminal state, ownership rules.RUNTIME.md—Execra,Command,JobHandle, subscription, cancellation, persistence.INTERPRETER.md— TheInterpretertrait and its execution model.examples/scoop_install.rsandexamples/scoop_doctor.rs— Reference interpreters covering phases, byte progress, multi-line collection, findings with typed actions, and exit-code classification.crates/execra-cliandcrates/execra-tauri— Adapter crates that consume this library.
License
Licensed under the Apache License, Version 2.0 (LICENSE.md or http://www.apache.org/licenses/LICENSE-2.0).
Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be licensed as above, without any additional terms or conditions.