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_contains, dispatch_assert_eq, dispatch_assign,
14 dispatch_assign_async_step, dispatch_assign_capture_step, dispatch_async_block,
15 dispatch_await_capture_step, dispatch_await_step, dispatch_break, dispatch_call,
16 dispatch_cancel_step, dispatch_continue, dispatch_copy, dispatch_copy_git, dispatch_cwd,
17 dispatch_echo, dispatch_env, dispatch_exit, dispatch_expand, dispatch_for_loop,
18 dispatch_func_def, dispatch_hash_sha256, dispatch_if_then, dispatch_inherit_env, dispatch_ls,
19 dispatch_mkdir, dispatch_read, dispatch_read_line, dispatch_return, dispatch_run,
20 dispatch_run_exec, dispatch_set, dispatch_sleep_step, dispatch_symlink, dispatch_timeout_step,
21 dispatch_while_loop, dispatch_with_io, dispatch_with_io_block, dispatch_workdir,
22 dispatch_workspace, dispatch_write,
23};
24pub use self::io::ExecIo;
25pub(crate) use self::steps::StepCtx;
26
27use anyhow::Result;
28use oxdock_fs::{
29 GuardedPath, LazyGuardedTempDir, PathResolver, WorkspaceFs, reserve_cargo_scratch,
30};
31use oxdock_parser::{Step, Value};
32use oxdock_process::{
33 BuiltinEnv, ProcessManager, SharedInput, SharedOutput, default_process_manager,
34};
35
36use std::collections::BTreeMap;
37use std::sync::Arc;
38
39use self::fs_ops::describe_dir;
40use self::io::{StreamHandle, assemble_default_io, teed_stderr, teed_stdout};
41use self::state::ExecState;
42use self::steps::execute_steps;
43
44pub const SNAPSHOT_PENDING_DISPLAY: &str = "<snapshot:pending>";
49
50pub const SNAPSHOT_TREE_UNAVAILABLE: &str = "<unavailable>";
54
55pub fn run_steps(fs_root: &GuardedPath, steps: &[Step]) -> Result<()> {
56 run_steps_with_context(fs_root, fs_root, steps)
57}
58
59pub fn run_steps_with_context(
60 fs_root: &GuardedPath,
61 build_context: &GuardedPath,
62 steps: &[Step],
63) -> Result<()> {
64 run_steps_with_context_result(fs_root, build_context, steps, None, None).map(|_| ())
65}
66
67pub fn run_steps_with_context_result(
69 fs_root: &GuardedPath,
70 build_context: &GuardedPath,
71 steps: &[Step],
72 stdin: Option<SharedInput>,
73 stdout: Option<SharedOutput>,
74) -> Result<GuardedPath> {
75 let io = assemble_default_io(stdin, stdout);
76 run_steps_with_context_result_with_io(fs_root, build_context, steps, io)
77}
78
79pub fn run_steps_with_context_result_with_io(
80 fs_root: &GuardedPath,
81 build_context: &GuardedPath,
82 steps: &[Step],
83 io: ExecIo,
84) -> Result<GuardedPath> {
85 match run_steps_inner(fs_root, build_context, steps, io) {
86 Ok(final_cwd) => Ok(final_cwd),
87 Err(err) => {
88 let fs = PathResolver::new(fs_root.as_path(), build_context.as_path())?;
89 let tree = describe_dir(&fs, fs_root, 2, 24);
90 let snapshot = format!(
91 "filesystem snapshot (root {}):\n{}",
92 fs_root.display(),
93 tree
94 );
95 Err(compose_error_with_snapshot(err, snapshot))
96 }
97 }
98}
99
100fn compose_error_with_snapshot(err: anyhow::Error, snapshot_section: String) -> anyhow::Error {
104 let chain = err.chain().map(|e| e.to_string()).collect::<Vec<_>>();
106 let mut primary = chain
107 .first()
108 .cloned()
109 .unwrap_or_else(|| "unknown error".into());
110 let rest = if chain.len() > 1 {
111 let first_cause = chain[1].clone();
112 primary = format!("{primary} ({first_cause})");
113 if chain.len() > 2 {
114 let causes = chain
115 .iter()
116 .skip(2)
117 .map(|s| s.as_str())
118 .collect::<Vec<_>>()
119 .join("\n ");
120 format!("\ncauses:\n {}", causes)
121 } else {
122 String::new()
123 }
124 } else {
125 String::new()
126 };
127 let msg = format!("{}{}\n{}", primary, rest, snapshot_section);
128 anyhow::anyhow!(msg)
129}
130
131fn run_steps_inner(
132 fs_root: &GuardedPath,
133 build_context: &GuardedPath,
134 steps: &[Step],
135 io: ExecIo,
136) -> Result<GuardedPath> {
137 let mut resolver = PathResolver::new_guarded(fs_root.clone(), build_context.clone())?;
138 resolver.set_workspace_root(build_context.clone());
139 run_steps_with_fs_with_io(Box::new(resolver), steps, io)
140}
141
142pub struct LazyRunOutput {
147 pub final_cwd: GuardedPath,
148 pub snapshot: Arc<LazyGuardedTempDir>,
149 pub fs: Box<dyn WorkspaceFs>,
150 pub bindings: BTreeMap<String, Value>,
155}
156
157pub fn run_steps_with_lazy_snapshot(
161 build_context: &GuardedPath,
162 steps: &[Step],
163 io: ExecIo,
164) -> Result<LazyRunOutput> {
165 let mut resolver = PathResolver::new_lazy(build_context.clone())?;
166 resolver.set_workspace_root(build_context.clone());
167 let snapshot = resolver.snapshot_handle();
168 let fs: Box<dyn WorkspaceFs> = Box::new(resolver);
169 match run_steps_with_manager(fs, steps, default_process_manager(), io) {
170 Ok((final_cwd, fs, bindings)) => Ok(LazyRunOutput {
171 final_cwd,
172 snapshot,
173 fs,
174 bindings,
175 }),
176 Err(err) => Err(enrich_lazy_error(&snapshot, build_context, err)),
177 }
178}
179
180pub fn enrich_lazy_error(
185 snapshot: &Arc<LazyGuardedTempDir>,
186 build_context: &GuardedPath,
187 err: anyhow::Error,
188) -> anyhow::Error {
189 match snapshot.get() {
190 Some(concrete) => {
191 let tree = match PathResolver::new(concrete.as_path(), build_context.as_path()) {
192 Ok(describe_fs) => describe_dir(&describe_fs, concrete, 2, 24),
193 Err(_) => String::from(SNAPSHOT_TREE_UNAVAILABLE),
194 };
195 let snapshot_msg = format!(
196 "filesystem snapshot (root {}):\n{}",
197 concrete.display(),
198 tree
199 );
200 compose_error_with_snapshot(err, snapshot_msg)
201 }
202 None => compose_error_with_snapshot(
203 err,
204 String::from(
205 "filesystem snapshot: never materialized (no snapshot directory was created)",
206 ),
207 ),
208 }
209}
210
211pub fn run_steps_with_fs(
212 fs: Box<dyn WorkspaceFs>,
213 steps: &[Step],
214 stdin: Option<SharedInput>,
215 stdout: Option<SharedOutput>,
216) -> Result<GuardedPath> {
217 let io = assemble_default_io(stdin, stdout);
218 run_steps_with_fs_with_io(fs, steps, io)
219}
220
221pub fn run_steps_with_fs_with_io(
222 fs: Box<dyn WorkspaceFs>,
223 steps: &[Step],
224 io: ExecIo,
225) -> Result<GuardedPath> {
226 run_steps_with_manager(fs, steps, default_process_manager(), io).map(|(cwd, _, _)| cwd)
227}
228
229#[allow(clippy::type_complexity)]
237pub fn run_steps_with_manager<P: ProcessManager>(
238 fs: Box<dyn WorkspaceFs>,
239 steps: &[Step],
240 process: P,
241 io: ExecIo,
242) -> Result<(GuardedPath, Box<dyn WorkspaceFs>, BTreeMap<String, Value>)> {
243 let cwd = fs.root().clone();
244 let build_context = fs.build_context().clone();
245 let mut envs = BuiltinEnv::collect(&build_context).into_envs();
246 for (key, value) in io.inherit_env_overrides() {
247 envs.insert(key.clone(), value.clone());
248 }
249 let envs = Arc::new(envs);
250 let assert_windows = Arc::new(std::sync::Mutex::new(std::collections::HashMap::new()));
251 let assert_windows_stderr = Arc::new(std::sync::Mutex::new(std::collections::HashMap::new()));
252 let exact_stdout = Arc::new(std::sync::Mutex::new(std::collections::HashMap::new()));
253 let mut state = ExecState {
254 fs,
255 cargo_scratch: reserve_cargo_scratch()?,
256 cwd,
257 envs,
258 bg_children: Vec::new(),
259 scope_stack: Vec::new(),
260 io,
261 assert_windows: assert_windows.clone(),
262 assert_windows_stderr: assert_windows_stderr.clone(),
263 exact_stdout: exact_stdout.clone(),
264 var_scopes: Vec::new(),
265 cancel_token: std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false)),
266 active_process: std::sync::Arc::new(std::sync::Mutex::new(None)),
267 named_tasks: std::sync::Arc::new(std::sync::Mutex::new(std::collections::HashMap::new())),
268 next_task_id: std::sync::Arc::new(std::sync::atomic::AtomicU64::new(0)),
269 inside_async: false,
270 keeper_expiry: None,
271 cancellable: false,
272 funcs: std::sync::Arc::new(std::collections::HashMap::new()),
273 host_funcs: std::sync::Arc::new(std::collections::HashMap::new()),
274 call_depth: 0,
275 _marker: std::marker::PhantomData,
276 };
277
278 state.push_var_scope();
280
281 let _default_stdout = std::io::stdout();
282 let stdin = state.io.stdin().into();
283 let stdout = Some(StreamHandle::Stream(teed_stdout(
287 state.io.stdout(),
288 assert_windows,
289 exact_stdout,
290 )));
291 let stderr = state
292 .io
293 .stderr()
294 .map(|sink| StreamHandle::Stream(teed_stderr(Some(sink), assert_windows_stderr)));
295 let mut proc_mgr = process;
296 let flow = execute_steps(
297 &mut state,
298 &mut proc_mgr,
299 steps,
300 stdin,
301 false,
302 stdout,
303 stderr,
304 true,
305 )?;
306 match flow {
307 self::steps::Flow::Done => {
312 let bindings: BTreeMap<String, Value> = state
317 .var_scopes
318 .first()
319 .map(|scope| {
320 scope
321 .iter()
322 .map(|(k, (_, v))| (k.clone(), v.clone()))
323 .collect()
324 })
325 .unwrap_or_default();
326 Ok((state.fs.concretize_cwd(&state.cwd), state.fs, bindings))
327 }
328 self::steps::Flow::Break { idx } => {
329 anyhow::bail!("step {}: BREAK outside loop", idx + 1)
330 }
331 self::steps::Flow::Continue { idx } => {
332 anyhow::bail!("step {}: CONTINUE outside loop", idx + 1)
333 }
334 self::steps::Flow::Return { idx, .. } => {
335 anyhow::bail!("step {}: RETURN outside function", idx + 1)
336 }
337 }
338}