Skip to main content

oxdock_core/exec/
mod.rs

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