Skip to main content

oxdock_core/exec/
mod.rs

1mod fs_ops;
2mod handlers;
3mod io;
4mod pipe;
5mod state;
6mod steps;
7#[cfg(test)]
8mod tests;
9
10pub use self::io::ExecIo;
11
12use anyhow::Result;
13use oxdock_fs::{GuardedPath, PathResolver, WorkspaceFs};
14use oxdock_parser::{Step, TemplateString};
15use oxdock_process::{
16    BuiltinEnv, CommandContext, ProcessManager, SharedInput, SharedOutput, default_process_manager,
17    expand_command_env,
18};
19
20use std::sync::Arc;
21
22use self::fs_ops::describe_dir;
23use self::io::{StreamHandle, assemble_default_io, teed_stdout};
24use self::state::ExecState;
25use self::steps::execute_steps;
26
27fn expand_template(t: &TemplateString, ctx: &CommandContext) -> String {
28    expand_command_env(&t.0, ctx)
29}
30
31pub fn run_steps(fs_root: &GuardedPath, steps: &[Step]) -> Result<()> {
32    run_steps_with_context(fs_root, fs_root, steps)
33}
34
35pub fn run_steps_with_context(
36    fs_root: &GuardedPath,
37    build_context: &GuardedPath,
38    steps: &[Step],
39) -> Result<()> {
40    run_steps_with_context_result(fs_root, build_context, steps, None, None).map(|_| ())
41}
42
43/// Execute the DSL and return the final working directory after all steps.
44pub fn run_steps_with_context_result(
45    fs_root: &GuardedPath,
46    build_context: &GuardedPath,
47    steps: &[Step],
48    stdin: Option<SharedInput>,
49    stdout: Option<SharedOutput>,
50) -> Result<GuardedPath> {
51    let io = assemble_default_io(stdin, stdout);
52    run_steps_with_context_result_with_io(fs_root, build_context, steps, io)
53}
54
55pub fn run_steps_with_context_result_with_io(
56    fs_root: &GuardedPath,
57    build_context: &GuardedPath,
58    steps: &[Step],
59    io: ExecIo,
60) -> Result<GuardedPath> {
61    match run_steps_inner(fs_root, build_context, steps, io) {
62        Ok(final_cwd) => Ok(final_cwd),
63        Err(err) => {
64            // Compose a single error message with the top cause plus a compact fs snapshot.
65            let chain = err.chain().map(|e| e.to_string()).collect::<Vec<_>>();
66            let mut primary = chain
67                .first()
68                .cloned()
69                .unwrap_or_else(|| "unknown error".into());
70            let rest = if chain.len() > 1 {
71                let first_cause = chain[1].clone();
72                primary = format!("{primary} ({first_cause})");
73                if chain.len() > 2 {
74                    let causes = chain
75                        .iter()
76                        .skip(2)
77                        .map(|s| s.as_str())
78                        .collect::<Vec<_>>()
79                        .join("\n  ");
80                    format!("\ncauses:\n  {}", causes)
81                } else {
82                    String::new()
83                }
84            } else {
85                String::new()
86            };
87            let fs = PathResolver::new(fs_root.as_path(), build_context.as_path())?;
88            let tree = describe_dir(&fs, fs_root, 2, 24);
89            let snapshot = format!(
90                "filesystem snapshot (root {}):\n{}",
91                fs_root.display(),
92                tree
93            );
94            let msg = format!("{}{}\n{}", primary, rest, snapshot);
95            Err(anyhow::anyhow!(msg))
96        }
97    }
98}
99
100fn run_steps_inner(
101    fs_root: &GuardedPath,
102    build_context: &GuardedPath,
103    steps: &[Step],
104    io: ExecIo,
105) -> Result<GuardedPath> {
106    let mut resolver = PathResolver::new_guarded(fs_root.clone(), build_context.clone())?;
107    resolver.set_workspace_root(build_context.clone());
108    run_steps_with_fs_with_io(Box::new(resolver), steps, io)
109}
110
111pub fn run_steps_with_fs(
112    fs: Box<dyn WorkspaceFs>,
113    steps: &[Step],
114    stdin: Option<SharedInput>,
115    stdout: Option<SharedOutput>,
116) -> Result<GuardedPath> {
117    let io = assemble_default_io(stdin, stdout);
118    run_steps_with_fs_with_io(fs, steps, io)
119}
120
121pub fn run_steps_with_fs_with_io(
122    fs: Box<dyn WorkspaceFs>,
123    steps: &[Step],
124    io: ExecIo,
125) -> Result<GuardedPath> {
126    run_steps_with_manager(fs, steps, default_process_manager(), io)
127}
128
129fn run_steps_with_manager<P: ProcessManager>(
130    fs: Box<dyn WorkspaceFs>,
131    steps: &[Step],
132    process: P,
133    io: ExecIo,
134) -> Result<GuardedPath> {
135    let fs_root = fs.root().clone();
136    let cwd = fs.root().clone();
137    let build_context = fs.build_context().clone();
138    let envs = Arc::new(BuiltinEnv::collect(&build_context).into_envs());
139    let stdout_log = Arc::new(std::sync::Mutex::new(Vec::new()));
140    let mut state = ExecState {
141        fs,
142        cargo_target_dir: fs_root.join(".cargo-target")?,
143        cwd,
144        envs,
145        bg_children: Vec::new(),
146        scope_stack: Vec::new(),
147        io,
148        stdout_log,
149    };
150
151    let _default_stdout = std::io::stdout();
152    let stdin = state.io.stdin();
153    // Every emitted byte flows through the tee so ASSERT_STDOUT sees both
154    // interpreter output and streamed child output, even when no capture
155    // sink was configured (forwarding to real stdout in that case).
156    let stdout = Some(StreamHandle::Stream(teed_stdout(
157        state.io.stdout(),
158        Arc::clone(&state.stdout_log),
159    )));
160    let stderr = state.io.stderr().map(StreamHandle::Stream);
161    let mut proc_mgr = process;
162    execute_steps(
163        &mut state,
164        &mut proc_mgr,
165        steps,
166        stdin,
167        false,
168        stdout,
169        stderr,
170        true,
171    )?;
172
173    Ok(state.cwd)
174}