1mod args;
2mod capture;
3mod engine;
4mod fs_ops;
5mod handlers;
6mod io;
7mod native;
8mod pipe;
9mod state;
10mod steps;
11#[cfg(test)]
12mod tests;
13mod typing;
14
15pub use self::engine::{Engine, EngineOutput};
16pub(crate) use self::handlers::{
17 dispatch_append, dispatch_assert_contains, dispatch_assert_eq, dispatch_assign,
18 dispatch_assign_async_step, dispatch_assign_capture_step, dispatch_async_block,
19 dispatch_await_capture_step, dispatch_await_step, dispatch_break, dispatch_call,
20 dispatch_cancel_step, dispatch_continue, dispatch_copy, dispatch_copy_git, dispatch_cwd,
21 dispatch_echo, dispatch_env, dispatch_exit, dispatch_expand, dispatch_for_loop,
22 dispatch_func_def, dispatch_hash_sha256, dispatch_if_then, dispatch_inherit_env, dispatch_ls,
23 dispatch_mkdir, dispatch_read, dispatch_read_line, dispatch_return, dispatch_run,
24 dispatch_run_exec, dispatch_set, dispatch_sleep_step, dispatch_symlink, dispatch_timeout_step,
25 dispatch_while_loop, dispatch_with_io, dispatch_with_io_block, dispatch_workdir,
26 dispatch_workspace, dispatch_write,
27};
28pub use self::io::ExecIo;
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, ProcessManager, SharedInput, SharedOutput, default_process_manager,
49};
50
51use std::collections::BTreeMap;
52use std::sync::Arc;
53
54use self::fs_ops::describe_dir;
55use self::io::{StreamHandle, assemble_default_io, teed_stderr, teed_stdout};
56use self::steps::execute_steps;
57
58pub const SNAPSHOT_PENDING_DISPLAY: &str = "<snapshot:pending>";
63
64pub const SNAPSHOT_TREE_UNAVAILABLE: &str = "<unavailable>";
68
69pub fn run_steps(fs_root: &GuardedPath, steps: &[Step]) -> Result<()> {
70 run_steps_with_context(fs_root, fs_root, steps)
71}
72
73pub fn run_steps_with_context(
74 fs_root: &GuardedPath,
75 build_context: &GuardedPath,
76 steps: &[Step],
77) -> Result<()> {
78 run_steps_with_context_result(fs_root, build_context, steps, None, None).map(|_| ())
79}
80
81pub fn run_steps_with_context_result(
83 fs_root: &GuardedPath,
84 build_context: &GuardedPath,
85 steps: &[Step],
86 stdin: Option<SharedInput>,
87 stdout: Option<SharedOutput>,
88) -> Result<GuardedPath> {
89 let io = assemble_default_io(stdin, stdout);
90 run_steps_with_context_result_with_io(fs_root, build_context, steps, io)
91}
92
93pub fn run_steps_with_context_result_with_io(
94 fs_root: &GuardedPath,
95 build_context: &GuardedPath,
96 steps: &[Step],
97 io: ExecIo,
98) -> Result<GuardedPath> {
99 match run_steps_inner(fs_root, build_context, steps, io) {
100 Ok(final_cwd) => Ok(final_cwd),
101 Err(err) => {
102 let fs = PathResolver::new(fs_root.as_path(), build_context.as_path())?;
103 let tree = describe_dir(&fs, fs_root, 2, 24);
104 let snapshot = format!(
105 "filesystem snapshot (root {}):\n{}",
106 fs_root.display(),
107 tree
108 );
109 Err(compose_error_with_snapshot(err, snapshot))
110 }
111 }
112}
113
114fn compose_error_with_snapshot(err: anyhow::Error, snapshot_section: String) -> anyhow::Error {
118 let chain = err.chain().map(|e| e.to_string()).collect::<Vec<_>>();
120 let mut primary = chain
121 .first()
122 .cloned()
123 .unwrap_or_else(|| "unknown error".into());
124 let rest = if chain.len() > 1 {
125 let first_cause = chain[1].clone();
126 primary = format!("{primary} ({first_cause})");
127 if chain.len() > 2 {
128 let causes = chain
129 .iter()
130 .skip(2)
131 .map(|s| s.as_str())
132 .collect::<Vec<_>>()
133 .join("\n ");
134 format!("\ncauses:\n {}", causes)
135 } else {
136 String::new()
137 }
138 } else {
139 String::new()
140 };
141 let msg = format!("{}{}\n{}", primary, rest, snapshot_section);
142 anyhow::anyhow!(msg)
143}
144
145fn run_steps_inner(
146 fs_root: &GuardedPath,
147 build_context: &GuardedPath,
148 steps: &[Step],
149 io: ExecIo,
150) -> Result<GuardedPath> {
151 let mut resolver = PathResolver::new_guarded(fs_root.clone(), build_context.clone())?;
152 resolver.set_workspace_root(build_context.clone());
153 run_steps_with_fs_with_io(Box::new(resolver), steps, io)
154}
155
156pub struct LazyRunOutput {
161 pub final_cwd: GuardedPath,
162 pub snapshot: Arc<LazyGuardedTempDir>,
163 pub fs: Box<dyn WorkspaceFs>,
164 pub bindings: BTreeMap<String, Value>,
169}
170
171pub fn run_steps_with_lazy_snapshot(
175 build_context: &GuardedPath,
176 steps: &[Step],
177 io: ExecIo,
178) -> Result<LazyRunOutput> {
179 let mut resolver = PathResolver::new_lazy(build_context.clone())?;
180 resolver.set_workspace_root(build_context.clone());
181 let snapshot = resolver.snapshot_handle();
182 let fs: Box<dyn WorkspaceFs> = Box::new(resolver);
183 match run_steps_with_manager(fs, steps, default_process_manager(), io) {
184 Ok((final_cwd, fs, bindings)) => Ok(LazyRunOutput {
185 final_cwd,
186 snapshot,
187 fs,
188 bindings,
189 }),
190 Err(err) => Err(enrich_lazy_error(&snapshot, build_context, err)),
191 }
192}
193
194pub fn enrich_lazy_error(
199 snapshot: &Arc<LazyGuardedTempDir>,
200 build_context: &GuardedPath,
201 err: anyhow::Error,
202) -> anyhow::Error {
203 match snapshot.get() {
204 Some(concrete) => {
205 let tree = match PathResolver::new(concrete.as_path(), build_context.as_path()) {
206 Ok(describe_fs) => describe_dir(&describe_fs, concrete, 2, 24),
207 Err(_) => String::from(SNAPSHOT_TREE_UNAVAILABLE),
208 };
209 let snapshot_msg = format!(
210 "filesystem snapshot (root {}):\n{}",
211 concrete.display(),
212 tree
213 );
214 compose_error_with_snapshot(err, snapshot_msg)
215 }
216 None => compose_error_with_snapshot(
217 err,
218 String::from(
219 "filesystem snapshot: never materialized (no snapshot directory was created)",
220 ),
221 ),
222 }
223}
224
225pub fn run_steps_with_fs(
226 fs: Box<dyn WorkspaceFs>,
227 steps: &[Step],
228 stdin: Option<SharedInput>,
229 stdout: Option<SharedOutput>,
230) -> Result<GuardedPath> {
231 let io = assemble_default_io(stdin, stdout);
232 run_steps_with_fs_with_io(fs, steps, io)
233}
234
235pub fn run_steps_with_fs_with_io(
236 fs: Box<dyn WorkspaceFs>,
237 steps: &[Step],
238 io: ExecIo,
239) -> Result<GuardedPath> {
240 run_steps_with_manager(fs, steps, default_process_manager(), io).map(|(cwd, _, _)| cwd)
241}
242
243pub(crate) use oxdock_parser::base_name;
248
249#[allow(clippy::type_complexity)]
257pub fn run_steps_with_manager<P: ProcessManager>(
258 fs: Box<dyn WorkspaceFs>,
259 steps: &[Step],
260 process: P,
261 io: ExecIo,
262) -> Result<(GuardedPath, Box<dyn WorkspaceFs>, BTreeMap<String, Value>)> {
263 run_steps_with_manager_with_modules(fs, steps, process, io, Vec::new(), Vec::new())
264}
265
266#[allow(clippy::type_complexity)]
274pub fn run_steps_with_manager_with_modules<P: ProcessManager>(
275 fs: Box<dyn WorkspaceFs>,
276 steps: &[Step],
277 process: P,
278 io: ExecIo,
279 modules: Vec<HostModule<P>>,
280 types: Vec<&'static self::typing::TypeDescriptor>,
281) -> Result<(GuardedPath, Box<dyn WorkspaceFs>, BTreeMap<String, Value>)> {
282 let mut state = new_state(fs, io)?;
283 for module in modules {
284 state.register_module(module);
285 }
286 for descriptor in types {
287 state.register_type(descriptor);
288 }
289 finish_run(state, process, steps)
290}
291
292fn new_state<P: ProcessManager>(fs: Box<dyn WorkspaceFs>, io: ExecIo) -> Result<ExecState<P>> {
293 let cwd = fs.root().clone();
294 let build_context = fs.build_context().clone();
295 let mut envs = BuiltinEnv::collect(&build_context).into_envs();
296 for (key, value) in io.inherit_env_overrides() {
297 envs.insert(key.clone(), value.clone());
298 }
299 let envs = Arc::new(envs);
300 let assert_windows = Arc::new(std::sync::Mutex::new(std::collections::HashMap::new()));
301 let assert_windows_stderr = Arc::new(std::sync::Mutex::new(std::collections::HashMap::new()));
302 let exact_stdout = Arc::new(std::sync::Mutex::new(std::collections::HashMap::new()));
303 let mut state = ExecState {
304 fs,
305 cargo_scratch: reserve_cargo_scratch()?,
306 cwd,
307 envs,
308 bg_children: Vec::new(),
309 scope_stack: Vec::new(),
310 io,
311 assert_windows: assert_windows.clone(),
312 assert_windows_stderr: assert_windows_stderr.clone(),
313 exact_stdout: exact_stdout.clone(),
314 var_scopes: Vec::new(),
315 cancel_token: std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false)),
316 active_process: std::sync::Arc::new(std::sync::Mutex::new(None)),
317 named_tasks: std::sync::Arc::new(std::sync::Mutex::new(std::collections::HashMap::new())),
318 next_task_id: std::sync::Arc::new(std::sync::atomic::AtomicU64::new(0)),
319 inside_async: false,
320 keeper_expiry: None,
321 cancellable: false,
322 functions: self::native::FunctionRegistry::with_builtins(),
323 types: self::typing::startup_type_map(),
324 call_depth: 0,
325 _marker: std::marker::PhantomData,
326 };
327
328 state.push_var_scope();
330
331 Ok(state)
332}
333
334#[allow(clippy::type_complexity)]
335fn finish_run<P: ProcessManager>(
336 mut state: ExecState<P>,
337 process: P,
338 steps: &[Step],
339) -> Result<(GuardedPath, Box<dyn WorkspaceFs>, BTreeMap<String, Value>)> {
340 let assert_windows = Arc::clone(&state.assert_windows);
341 let assert_windows_stderr = Arc::clone(&state.assert_windows_stderr);
342 let exact_stdout = Arc::clone(&state.exact_stdout);
343 let _default_stdout = std::io::stdout();
344 let stdin = state.io.stdin().into();
345 let stdout = Some(StreamHandle::Stream(teed_stdout(
349 state.io.stdout(),
350 assert_windows,
351 exact_stdout,
352 )));
353 let stderr = state
354 .io
355 .stderr()
356 .map(|sink| StreamHandle::Stream(teed_stderr(Some(sink), assert_windows_stderr)));
357 let mut proc_mgr = process;
358 let flow = execute_steps(
359 &mut state,
360 &mut proc_mgr,
361 steps,
362 stdin,
363 false,
364 stdout,
365 stderr,
366 true,
367 )?;
368 match flow {
369 self::steps::Flow::Done => {
374 let bindings: BTreeMap<String, Value> = state
379 .var_scopes
380 .first()
381 .map(|scope| {
382 scope
383 .iter()
384 .map(|(k, (_, v))| (k.clone(), v.clone()))
385 .collect()
386 })
387 .unwrap_or_default();
388 Ok((state.fs.concretize_cwd(&state.cwd), state.fs, bindings))
389 }
390 self::steps::Flow::Break { idx } => {
391 anyhow::bail!("step {}: BREAK outside loop", idx + 1)
392 }
393 self::steps::Flow::Continue { idx } => {
394 anyhow::bail!("step {}: CONTINUE outside loop", idx + 1)
395 }
396 self::steps::Flow::Return { idx, .. } => {
397 anyhow::bail!("step {}: RETURN outside function", idx + 1)
398 }
399 }
400}