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