Skip to main content

oxdock_core/exec/
mod.rs

1mod args;
2mod capture;
3mod fs_ops;
4mod handlers;
5mod io;
6mod pipe;
7mod state;
8mod steps;
9#[cfg(test)]
10mod tests;
11
12pub(crate) use self::handlers::{
13    dispatch_append, dispatch_assert_absent, dispatch_assert_dir, dispatch_assert_file,
14    dispatch_assert_stdout, dispatch_assign, dispatch_assign_async_step,
15    dispatch_assign_capture_step, dispatch_async_block, dispatch_await_capture_step,
16    dispatch_await_step, dispatch_break, dispatch_call, dispatch_cancel_step, dispatch_continue,
17    dispatch_copy, dispatch_copy_git, dispatch_cwd, dispatch_echo, dispatch_env, dispatch_exit,
18    dispatch_expand, dispatch_for_loop, dispatch_func_def, dispatch_hash_sha256, dispatch_if_then,
19    dispatch_inherit_env, dispatch_ls, dispatch_mkdir, dispatch_read, dispatch_read_line,
20    dispatch_return, dispatch_run, dispatch_run_exec, dispatch_set, dispatch_sleep_step,
21    dispatch_symlink, dispatch_timeout_step, dispatch_while_loop, dispatch_with_io,
22    dispatch_with_io_block, dispatch_workdir, dispatch_workspace, dispatch_write,
23};
24pub use self::io::ExecIo;
25pub(crate) use self::steps::StepCtx;
26
27use anyhow::Result;
28use oxdock_fs::{GuardedPath, PathResolver, WorkspaceFs};
29use oxdock_parser::Step;
30use oxdock_process::{
31    BuiltinEnv, ProcessManager, SharedInput, SharedOutput, default_process_manager,
32};
33
34use std::sync::Arc;
35
36use self::fs_ops::describe_dir;
37use self::io::{StreamHandle, assemble_default_io, teed_stdout};
38use self::state::ExecState;
39use self::steps::execute_steps;
40
41pub fn run_steps(fs_root: &GuardedPath, steps: &[Step]) -> Result<()> {
42    run_steps_with_context(fs_root, fs_root, steps)
43}
44
45pub fn run_steps_with_context(
46    fs_root: &GuardedPath,
47    build_context: &GuardedPath,
48    steps: &[Step],
49) -> Result<()> {
50    run_steps_with_context_result(fs_root, build_context, steps, None, None).map(|_| ())
51}
52
53/// Execute the DSL and return the final working directory after all steps.
54pub fn run_steps_with_context_result(
55    fs_root: &GuardedPath,
56    build_context: &GuardedPath,
57    steps: &[Step],
58    stdin: Option<SharedInput>,
59    stdout: Option<SharedOutput>,
60) -> Result<GuardedPath> {
61    let io = assemble_default_io(stdin, stdout);
62    run_steps_with_context_result_with_io(fs_root, build_context, steps, io)
63}
64
65pub fn run_steps_with_context_result_with_io(
66    fs_root: &GuardedPath,
67    build_context: &GuardedPath,
68    steps: &[Step],
69    io: ExecIo,
70) -> Result<GuardedPath> {
71    match run_steps_inner(fs_root, build_context, steps, io) {
72        Ok(final_cwd) => Ok(final_cwd),
73        Err(err) => {
74            // Compose a single error message with the top cause plus a compact fs snapshot.
75            let chain = err.chain().map(|e| e.to_string()).collect::<Vec<_>>();
76            let mut primary = chain
77                .first()
78                .cloned()
79                .unwrap_or_else(|| "unknown error".into());
80            let rest = if chain.len() > 1 {
81                let first_cause = chain[1].clone();
82                primary = format!("{primary} ({first_cause})");
83                if chain.len() > 2 {
84                    let causes = chain
85                        .iter()
86                        .skip(2)
87                        .map(|s| s.as_str())
88                        .collect::<Vec<_>>()
89                        .join("\n  ");
90                    format!("\ncauses:\n  {}", causes)
91                } else {
92                    String::new()
93                }
94            } else {
95                String::new()
96            };
97            let fs = PathResolver::new(fs_root.as_path(), build_context.as_path())?;
98            let tree = describe_dir(&fs, fs_root, 2, 24);
99            let snapshot = format!(
100                "filesystem snapshot (root {}):\n{}",
101                fs_root.display(),
102                tree
103            );
104            let msg = format!("{}{}\n{}", primary, rest, snapshot);
105            Err(anyhow::anyhow!(msg))
106        }
107    }
108}
109
110fn run_steps_inner(
111    fs_root: &GuardedPath,
112    build_context: &GuardedPath,
113    steps: &[Step],
114    io: ExecIo,
115) -> Result<GuardedPath> {
116    let mut resolver = PathResolver::new_guarded(fs_root.clone(), build_context.clone())?;
117    resolver.set_workspace_root(build_context.clone());
118    run_steps_with_fs_with_io(Box::new(resolver), steps, io)
119}
120
121pub fn run_steps_with_fs(
122    fs: Box<dyn WorkspaceFs>,
123    steps: &[Step],
124    stdin: Option<SharedInput>,
125    stdout: Option<SharedOutput>,
126) -> Result<GuardedPath> {
127    let io = assemble_default_io(stdin, stdout);
128    run_steps_with_fs_with_io(fs, steps, io)
129}
130
131pub fn run_steps_with_fs_with_io(
132    fs: Box<dyn WorkspaceFs>,
133    steps: &[Step],
134    io: ExecIo,
135) -> Result<GuardedPath> {
136    run_steps_with_manager(fs, steps, default_process_manager(), io)
137}
138
139fn run_steps_with_manager<P: ProcessManager>(
140    fs: Box<dyn WorkspaceFs>,
141    steps: &[Step],
142    process: P,
143    io: ExecIo,
144) -> Result<GuardedPath> {
145    let fs_root = fs.root().clone();
146    let cwd = fs.root().clone();
147    let build_context = fs.build_context().clone();
148    let mut envs = BuiltinEnv::collect(&build_context).into_envs();
149    for (key, value) in io.inherit_env_overrides() {
150        envs.insert(key.clone(), value.clone());
151    }
152    let envs = Arc::new(envs);
153    let assert_windows = Arc::new(std::sync::Mutex::new(std::collections::HashMap::new()));
154    let mut state = ExecState {
155        fs,
156        cargo_target_dir: fs_root.join(".cargo-target")?,
157        cwd,
158        envs,
159        bg_children: Vec::new(),
160        scope_stack: Vec::new(),
161        io,
162        assert_windows: assert_windows.clone(),
163        var_scopes: Vec::new(),
164        cancel_token: std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false)),
165        active_process: std::sync::Arc::new(std::sync::Mutex::new(None)),
166        named_tasks: std::sync::Arc::new(std::sync::Mutex::new(std::collections::HashMap::new())),
167        next_task_id: std::sync::Arc::new(std::sync::atomic::AtomicU64::new(0)),
168        inside_async: false,
169        keeper_expiry: None,
170        cancellable: false,
171        funcs: std::sync::Arc::new(std::collections::HashMap::new()),
172        host_funcs: std::sync::Arc::new(std::collections::HashMap::new()),
173        call_depth: 0,
174        _marker: std::marker::PhantomData,
175    };
176
177    // Push a global variable scope so top-level LET assignments are captured.
178    state.push_var_scope();
179
180    let _default_stdout = std::io::stdout();
181    let stdin = state.io.stdin().into();
182    // Every emitted byte flows through the tee so ASSERT_STDOUT sees both
183    // interpreter output and streamed child output, even when no capture
184    // sink was configured (forwarding to real stdout in that case).
185    let stdout = Some(StreamHandle::Stream(teed_stdout(
186        state.io.stdout(),
187        assert_windows,
188    )));
189    let stderr = state.io.stderr().map(StreamHandle::Stream);
190    let mut proc_mgr = process;
191    let flow = execute_steps(
192        &mut state,
193        &mut proc_mgr,
194        steps,
195        stdin,
196        false,
197        stdout,
198        stderr,
199        true,
200    )?;
201    match flow {
202        self::steps::Flow::Done => Ok(state.cwd),
203        self::steps::Flow::Break { idx } => {
204            anyhow::bail!("step {}: BREAK outside loop", idx + 1)
205        }
206        self::steps::Flow::Continue { idx } => {
207            anyhow::bail!("step {}: CONTINUE outside loop", idx + 1)
208        }
209        self::steps::Flow::Return { idx, .. } => {
210            anyhow::bail!("step {}: RETURN outside function", idx + 1)
211        }
212    }
213}