1mod args;
2mod capture;
3mod engine;
4mod fs_ops;
5mod handlers;
6mod io;
7mod native;
8mod state;
9mod steps;
10#[cfg(test)]
11mod tests;
12mod typing;
13
14pub use self::engine::{Engine, EngineOutput};
15pub(crate) use self::handlers::{
16 dispatch_append, dispatch_assert_contains, dispatch_assert_eq, dispatch_assign,
17 dispatch_assign_async_step, dispatch_assign_capture_step, dispatch_async_block,
18 dispatch_await_capture_step, dispatch_await_step, dispatch_break, dispatch_call,
19 dispatch_cancel_step, dispatch_continue, dispatch_copy, dispatch_copy_git, dispatch_cwd,
20 dispatch_echo, dispatch_env, dispatch_exit, dispatch_expand, dispatch_for_loop,
21 dispatch_func_def, dispatch_hash_sha256, dispatch_if_then, dispatch_inherit_env, dispatch_ls,
22 dispatch_mkdir, dispatch_push_into_step, dispatch_read, dispatch_read_line, dispatch_return,
23 dispatch_run, dispatch_run_exec, dispatch_set, dispatch_sleep_step, dispatch_symlink,
24 dispatch_timeout_step, dispatch_while_loop, dispatch_with_io, dispatch_with_io_block,
25 dispatch_workdir, dispatch_workspace, dispatch_write,
26};
27pub use self::io::ExecIo;
28pub use self::io::PipeStream;
29pub use self::native::{
30 FuncKind, FuncMeta, FuncParam, FunctionRegistry, HostModule, HostRegistration, NativeFn,
31 OxDockFn, PureFn, builtin_function_metas, builtin_function_names, std_module_table,
32};
33pub use self::state::ExecState;
34pub use self::steps::StepCtx;
35pub use self::typing::{
36 OxDockType, TypeDescriptor, Value, ValuePayload, clone_boxed, clone_copy, clone_shared,
37 drop_boxed, drop_noop, drop_shared, eq_boxed, eq_inline, eq_shared, fmt_boxed, fmt_inline,
38 fmt_shared, load_inline, startup_descriptors, store_inline, type_anchor, unshare_boxed,
39 unshare_inline, unshare_shared,
40};
41
42use anyhow::Result;
43use oxdock_fs::{
44 GuardedPath, LazyGuardedTempDir, PathResolver, WorkspaceFs, reserve_cargo_scratch,
45};
46use oxdock_parser::Step;
47use oxdock_process::{
48 BuiltinEnv, DefaultProcessManager, ProcessManager, SharedInput, SharedOutput,
49 default_process_manager,
50};
51
52use std::collections::BTreeMap;
53use std::sync::Arc;
54
55use self::fs_ops::describe_dir;
56use self::io::{StreamHandle, assemble_default_io, teed_stderr, teed_stdout};
57use self::steps::execute_steps;
58
59pub const SNAPSHOT_PENDING_DISPLAY: &str = "<snapshot:pending>";
64
65pub const SNAPSHOT_TREE_UNAVAILABLE: &str = "<unavailable>";
69
70pub fn run_steps(fs_root: &GuardedPath, steps: &[Step]) -> Result<()> {
71 run_steps_with_context(fs_root, fs_root, steps)
72}
73
74pub fn run_steps_with_context(
75 fs_root: &GuardedPath,
76 build_context: &GuardedPath,
77 steps: &[Step],
78) -> Result<()> {
79 run_steps_with_context_result(fs_root, build_context, steps, None, None).map(|_| ())
80}
81
82pub fn run_steps_with_context_result(
84 fs_root: &GuardedPath,
85 build_context: &GuardedPath,
86 steps: &[Step],
87 stdin: Option<SharedInput>,
88 stdout: Option<SharedOutput>,
89) -> Result<GuardedPath> {
90 let io = assemble_default_io(stdin, stdout);
91 run_steps_with_context_result_with_io(fs_root, build_context, steps, io)
92}
93
94pub fn run_steps_with_context_result_with_io(
95 fs_root: &GuardedPath,
96 build_context: &GuardedPath,
97 steps: &[Step],
98 io: ExecIo,
99) -> Result<GuardedPath> {
100 match run_steps_inner(fs_root, build_context, steps, io) {
101 Ok(final_cwd) => Ok(final_cwd),
102 Err(err) => {
103 let fs = PathResolver::new(fs_root.as_path(), build_context.as_path())?;
104 let tree = describe_dir(&fs, fs_root, 2, 24);
105 let snapshot = format!(
106 "filesystem snapshot (root {}):\n{}",
107 fs_root.display(),
108 tree
109 );
110 Err(compose_error_with_snapshot(err, snapshot))
111 }
112 }
113}
114
115fn compose_error_with_snapshot(err: anyhow::Error, snapshot_section: String) -> anyhow::Error {
119 let chain = err.chain().map(|e| e.to_string()).collect::<Vec<_>>();
121 let mut primary = chain
122 .first()
123 .cloned()
124 .unwrap_or_else(|| "unknown error".into());
125 let rest = if chain.len() > 1 {
126 let first_cause = chain[1].clone();
127 primary = format!("{primary} ({first_cause})");
128 if chain.len() > 2 {
129 let causes = chain
130 .iter()
131 .skip(2)
132 .map(|s| s.as_str())
133 .collect::<Vec<_>>()
134 .join("\n ");
135 format!("\ncauses:\n {}", causes)
136 } else {
137 String::new()
138 }
139 } else {
140 String::new()
141 };
142 let msg = format!("{}{}\n{}", primary, rest, snapshot_section);
143 anyhow::anyhow!(msg)
144}
145
146fn run_steps_inner(
147 fs_root: &GuardedPath,
148 build_context: &GuardedPath,
149 steps: &[Step],
150 io: ExecIo,
151) -> Result<GuardedPath> {
152 let mut resolver = PathResolver::new_guarded(fs_root.clone(), build_context.clone())?;
153 resolver.set_workspace_root(build_context.clone());
154 run_steps_with_fs_with_io(Box::new(resolver), steps, io)
155}
156
157pub struct LazyRunOutput {
162 pub final_cwd: GuardedPath,
163 pub snapshot: Arc<LazyGuardedTempDir>,
164 pub fs: Box<dyn WorkspaceFs>,
165 pub bindings: BTreeMap<String, Value>,
170}
171
172pub fn run_steps_with_lazy_snapshot(
176 build_context: &GuardedPath,
177 steps: &[Step],
178 io: ExecIo,
179) -> Result<LazyRunOutput> {
180 run_steps_with_lazy_snapshot_and_modules(build_context, steps, io, Vec::new(), Vec::new())
181}
182
183pub fn run_steps_with_lazy_snapshot_and_modules(
188 build_context: &GuardedPath,
189 steps: &[Step],
190 io: ExecIo,
191 modules: Vec<HostModule<DefaultProcessManager>>,
192 types: Vec<&'static TypeDescriptor>,
193) -> Result<LazyRunOutput> {
194 let mut resolver = PathResolver::new_lazy(build_context.clone())?;
195 resolver.set_workspace_root(build_context.clone());
196 let snapshot = resolver.snapshot_handle();
197 let fs: Box<dyn WorkspaceFs> = Box::new(resolver);
198 match run_steps_with_manager_with_modules(
199 fs,
200 steps,
201 default_process_manager(),
202 io,
203 modules,
204 types,
205 ) {
206 Ok((final_cwd, fs, bindings)) => Ok(LazyRunOutput {
207 final_cwd,
208 snapshot,
209 fs,
210 bindings,
211 }),
212 Err(err) => Err(enrich_lazy_error(&snapshot, build_context, err)),
213 }
214}
215
216pub fn enrich_lazy_error(
221 snapshot: &Arc<LazyGuardedTempDir>,
222 build_context: &GuardedPath,
223 err: anyhow::Error,
224) -> anyhow::Error {
225 match snapshot.get() {
226 Some(concrete) => {
227 let tree = match PathResolver::new(concrete.as_path(), build_context.as_path()) {
228 Ok(describe_fs) => describe_dir(&describe_fs, concrete, 2, 24),
229 Err(_) => String::from(SNAPSHOT_TREE_UNAVAILABLE),
230 };
231 let snapshot_msg = format!(
232 "filesystem snapshot (root {}):\n{}",
233 concrete.display(),
234 tree
235 );
236 compose_error_with_snapshot(err, snapshot_msg)
237 }
238 None => compose_error_with_snapshot(
239 err,
240 String::from(
241 "filesystem snapshot: never materialized (no snapshot directory was created)",
242 ),
243 ),
244 }
245}
246
247pub fn run_steps_with_fs(
248 fs: Box<dyn WorkspaceFs>,
249 steps: &[Step],
250 stdin: Option<SharedInput>,
251 stdout: Option<SharedOutput>,
252) -> Result<GuardedPath> {
253 let io = assemble_default_io(stdin, stdout);
254 run_steps_with_fs_with_io(fs, steps, io)
255}
256
257pub fn run_steps_with_fs_with_io(
258 fs: Box<dyn WorkspaceFs>,
259 steps: &[Step],
260 io: ExecIo,
261) -> Result<GuardedPath> {
262 run_steps_with_manager(fs, steps, default_process_manager(), io).map(|(cwd, _, _)| cwd)
263}
264
265pub(crate) use oxdock_parser::base_name;
270
271#[allow(clippy::type_complexity)]
279pub fn run_steps_with_manager<P: ProcessManager>(
280 fs: Box<dyn WorkspaceFs>,
281 steps: &[Step],
282 process: P,
283 io: ExecIo,
284) -> Result<(GuardedPath, Box<dyn WorkspaceFs>, BTreeMap<String, Value>)> {
285 run_steps_with_manager_with_modules(fs, steps, process, io, Vec::new(), Vec::new())
286}
287
288#[allow(clippy::type_complexity)]
296pub fn run_steps_with_manager_with_modules<P: ProcessManager>(
297 fs: Box<dyn WorkspaceFs>,
298 steps: &[Step],
299 process: P,
300 io: ExecIo,
301 modules: Vec<HostModule<P>>,
302 types: Vec<&'static self::typing::TypeDescriptor>,
303) -> Result<(GuardedPath, Box<dyn WorkspaceFs>, BTreeMap<String, Value>)> {
304 let mut state = new_state(fs, io)?;
305 for module in modules {
306 state.register_module(module);
307 }
308 for descriptor in types {
309 state.register_type(descriptor);
310 }
311 finish_run(state, process, steps)
312}
313
314fn new_state<P: ProcessManager>(fs: Box<dyn WorkspaceFs>, io: ExecIo) -> Result<ExecState<P>> {
315 let cwd = fs.root().clone();
316 let build_context = fs.build_context().clone();
317 let mut envs = BuiltinEnv::collect(&build_context).into_envs();
318 for (key, value) in io.inherit_env_overrides() {
319 envs.insert(key.clone(), value.clone());
320 }
321 let envs = Arc::new(envs);
322 let assert_windows = Arc::new(std::sync::Mutex::new(std::collections::HashMap::new()));
323 let assert_windows_stderr = Arc::new(std::sync::Mutex::new(std::collections::HashMap::new()));
324 let exact_stdout = Arc::new(std::sync::Mutex::new(std::collections::HashMap::new()));
325 let mut state = ExecState {
326 fs,
327 cargo_scratch: reserve_cargo_scratch()?,
328 cwd,
329 envs,
330 bg_children: Vec::new(),
331 scope_stack: Vec::new(),
332 io,
333 assert_windows: assert_windows.clone(),
334 assert_windows_stderr: assert_windows_stderr.clone(),
335 exact_stdout: exact_stdout.clone(),
336 var_scopes: Vec::new(),
337 cancel_token: std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false)),
338 active_process: std::sync::Arc::new(std::sync::Mutex::new(None)),
339 named_tasks: std::sync::Arc::new(std::sync::Mutex::new(std::collections::HashMap::new())),
340 next_task_id: std::sync::Arc::new(std::sync::atomic::AtomicU64::new(1)),
341 inside_async: false,
342 keeper_expiry: None,
343 cancellable: false,
344 functions: self::native::FunctionRegistry::with_builtins(),
345 types: self::typing::startup_type_map(),
346 call_depth: 0,
347 task_id: 0,
349 _marker: std::marker::PhantomData,
350 };
351
352 state.push_var_scope();
354
355 Ok(state)
356}
357
358#[allow(clippy::type_complexity)]
359fn finish_run<P: ProcessManager>(
360 mut state: ExecState<P>,
361 process: P,
362 steps: &[Step],
363) -> Result<(GuardedPath, Box<dyn WorkspaceFs>, BTreeMap<String, Value>)> {
364 let assert_windows = Arc::clone(&state.assert_windows);
365 let assert_windows_stderr = Arc::clone(&state.assert_windows_stderr);
366 let exact_stdout = Arc::clone(&state.exact_stdout);
367 let _default_stdout = std::io::stdout();
368 let stdin = state.io.stdin().into();
369 let stdout = Some(StreamHandle::Stream(teed_stdout(
373 state.io.stdout(),
374 assert_windows,
375 exact_stdout,
376 )));
377 let stderr = state
378 .io
379 .stderr()
380 .map(|sink| StreamHandle::Stream(teed_stderr(Some(sink), assert_windows_stderr)));
381 let mut proc_mgr = process;
382 let flow = execute_steps(
383 &mut state,
384 &mut proc_mgr,
385 steps,
386 stdin,
387 false,
388 stdout,
389 stderr,
390 true,
391 )?;
392 match flow {
393 self::steps::Flow::Done => {
398 let bindings: BTreeMap<String, Value> = state
403 .var_scopes
404 .first()
405 .map(|scope| {
406 scope
407 .iter()
408 .map(|(k, (_, v))| (k.clone(), v.clone()))
409 .collect()
410 })
411 .unwrap_or_default();
412 Ok((state.fs.concretize_cwd(&state.cwd), state.fs, bindings))
413 }
414 self::steps::Flow::Break { idx } => {
415 anyhow::bail!("step {}: BREAK outside loop", idx + 1)
416 }
417 self::steps::Flow::Continue { idx } => {
418 anyhow::bail!("step {}: CONTINUE outside loop", idx + 1)
419 }
420 self::steps::Flow::Return { idx, .. } => {
421 anyhow::bail!(
425 "step {}: RETURN outside function, ASYNC task, or LET block",
426 idx + 1
427 )
428 }
429 }
430}