Skip to content

Commit 75ef615

Browse files
committed
feat(stdlib/bun): bun_run_capture — captured child-process run
1 parent e8f6a71 commit 75ef615

4 files changed

Lines changed: 54 additions & 1 deletion

File tree

‎lib/codegen_deno.ml‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1339,6 +1339,18 @@ let () =
13391339
Printf.sprintf
13401340
"(__as_childProcess().spawnSync(%s, %s, { stdio: \"inherit\" }).status ?? 1)"
13411341
(arg 0 a) (arg 1 a));
1342+
(* Captured run: stdin closed, stdout/stderr piped as UTF-8. [cwd] is an
1343+
AffineScript Option ({ tag, value }). A host spawn error (ENOENT,
1344+
ENOBUFS) has no status, so it maps to 1 with its message on stderr. *)
1345+
b "bun_run_capture"
1346+
(fun a ->
1347+
Printf.sprintf
1348+
"((__r) => ({ status: __r.status ?? 1, stdout: __r.stdout ?? \"\", \
1349+
stderr: (__r.stderr ?? \"\") + (__r.error ? String(__r.error.message) : \"\") }))\
1350+
(__as_childProcess().spawnSync(%s, %s, { encoding: \"utf8\", \
1351+
stdio: [\"ignore\", \"pipe\", \"pipe\"], \
1352+
cwd: ((__c) => __c.tag === \"Some\" ? __c.value : undefined)(%s) }))"
1353+
(arg 0 a) (arg 1 a) (arg 2 a));
13421354
b "bun_stdin_text"
13431355
(fun _ -> "__as_fs().readFileSync(0, \"utf8\")");
13441356
b "bun_stdout_write"

‎stdlib/Bun.affine‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,23 @@ pub extern fn bun_env_get(name: String) -> Option<String>;
1616
/// its exit status (or 1 when the host supplies no status).
1717
pub extern fn bun_run(command: String, arguments: [String]) -> Int;
1818

19+
/// The captured outcome of a child process: its exit status (1 when the host
20+
/// supplies none, e.g. on a signal or a spawn failure) and its standard output
21+
/// and standard error decoded as UTF-8.
22+
pub type RunResult = {
23+
status: Int,
24+
stdout: String,
25+
stderr: String
26+
}
27+
28+
/// Run a child process synchronously and capture its standard output and
29+
/// standard error instead of inheriting them. Standard input is closed.
30+
/// `cwd` sets the child's working directory; `None` keeps the caller's. A
31+
/// spawn failure (missing program, output over the host buffer limit) yields
32+
/// status 1 with the host's error message appended to `stderr`.
33+
pub extern fn bun_run_capture(command: String, arguments: [String],
34+
cwd: Option<String>) -> RunResult;
35+
1936
/// Read standard input to EOF as UTF-8.
2037
pub extern fn bun_stdin_text() -> String;
2138

‎tests/codegen-bun/host_profile.affine‎

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// SPDX-License-Identifier: MPL-2.0
22
// Bun-ESM host-profile acceptance fixture (issue #734).
33

4-
use Bun::{ bun_env_get, bun_run, bun_stdin_text, bun_stdout_write, bun_stderr_write, bun_exit };
4+
use Bun::{ RunResult, bun_env_get, bun_run, bun_run_capture, bun_stdin_text, bun_stdout_write, bun_stderr_write, bun_exit };
55
use prelude::{Some, None};
66

77
extern type Bytes;
@@ -49,3 +49,14 @@ pub fn first_byte(path: String) -> Int {
4949
let value = readFileBytes(path);
5050
if bytesLength(value) == 0 { 0 } else { bytesByteAt(value, 0) }
5151
}
52+
53+
/// Run `sh -c script` in `cwd` (empty string = inherit) and capture the result.
54+
pub fn run_captured(script: String, cwd: String) -> RunResult {
55+
let dir = if cwd == "" { None } else { Some(cwd) };
56+
bun_run_capture("sh", ["-c", script], dir)
57+
}
58+
59+
/// Capture-run a program that does not exist, to exercise the spawn-error path.
60+
pub fn run_missing_program() -> RunResult {
61+
bun_run_capture("affinescript-no-such-program", [], None)
62+
}

‎tests/codegen-bun/host_profile.harness.mjs‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,19 @@ try {
2424
throw new Error("missing environment value failed");
2525
}
2626
if (subject.run_successful_child() !== 0) throw new Error("subprocess lowering failed");
27+
// Captured run (bun_run_capture): planted controls with known answers.
28+
const captured = subject.run_captured("echo hi; echo err >&2; exit 3", "");
29+
if (captured.status !== 3) throw new Error(`capture status failed: ${captured.status}`);
30+
if (captured.stdout !== "hi\n") throw new Error(`capture stdout failed: ${JSON.stringify(captured.stdout)}`);
31+
if (captured.stderr !== "err\n") throw new Error(`capture stderr failed: ${JSON.stringify(captured.stderr)}`);
32+
const inDir = subject.run_captured("pwd", nested);
33+
if (inDir.status !== 0 || inDir.stdout.trim() !== nested) {
34+
throw new Error(`capture cwd failed: ${JSON.stringify(inDir)}`);
35+
}
36+
const missing = subject.run_missing_program();
37+
if (missing.status !== 1 || !missing.stderr.includes("affinescript-no-such-program")) {
38+
throw new Error(`capture spawn-error failed: ${JSON.stringify(missing)}`);
39+
}
2740
subject.remove_path(file);
2841
let missingPathThrew = false;
2942
try {

0 commit comments

Comments
 (0)