1use anyhow::{Context, Result, bail};
2use std::collections::HashMap;
3use std::fs;
4use std::io::{self, Write};
5use std::path::{Path, PathBuf};
6use std::process::{Child, Command as ProcessCommand, ExitStatus};
7
8use crate::ast::{Step, StepKind, WorkspaceTarget};
9use crate::resolver::PathResolver;
10
11pub fn run_steps(fs_root: &Path, steps: &[Step]) -> Result<()> {
12 run_steps_with_context(fs_root, fs_root, steps)
13}
14
15pub fn run_steps_with_context(fs_root: &Path, build_context: &Path, steps: &[Step]) -> Result<()> {
16 run_steps_with_context_result(fs_root, build_context, steps).map(|_| ())
17}
18
19pub fn run_steps_with_context_result(
21 fs_root: &Path,
22 build_context: &Path,
23 steps: &[Step],
24) -> Result<PathBuf> {
25 match run_steps_inner(fs_root, build_context, steps) {
26 Ok(final_cwd) => Ok(final_cwd),
27 Err(err) => {
28 let chain = err.chain().map(|e| e.to_string()).collect::<Vec<_>>();
30 let primary = chain
31 .first()
32 .cloned()
33 .unwrap_or_else(|| "unknown error".into());
34 let rest = if chain.len() > 1 {
35 let causes = chain
36 .iter()
37 .skip(1)
38 .map(|s| s.as_str())
39 .collect::<Vec<_>>()
40 .join("\n ");
41 format!("\ncauses:\n {}", causes)
42 } else {
43 String::new()
44 };
45 let tree = describe_dir(fs_root, 2, 24);
46 let snapshot = format!(
47 "filesystem snapshot (root {}):\n{}",
48 fs_root.display(),
49 tree
50 );
51 let msg = format!("{}{}\n{}", primary, rest, snapshot);
52 Err(anyhow::anyhow!(msg))
53 }
54 }
55}
56
57fn run_steps_inner(fs_root: &Path, build_context: &Path, steps: &[Step]) -> Result<PathBuf> {
58 let cargo_target_dir = fs_root.join(".cargo-target");
59 let mut resolver = PathResolver::new(fs_root, build_context);
60 let mut cwd = resolver.root().to_path_buf();
61 let mut envs: HashMap<String, String> = HashMap::new();
62 let mut bg_children: Vec<Child> = Vec::new();
63
64 let check_bg = |bg: &mut Vec<Child>| -> Result<Option<ExitStatus>> {
65 let mut finished: Option<ExitStatus> = None;
66 for child in bg.iter_mut() {
67 if let Some(status) = child.try_wait()? {
68 finished = Some(status);
69 break;
70 }
71 }
72 if let Some(status) = finished {
73 for child in bg.iter_mut() {
75 if child.try_wait()?.is_none() {
76 let _ = child.kill();
77 let _ = child.wait();
78 }
79 }
80 bg.clear();
81 return Ok(Some(status));
82 }
83 Ok(None)
84 };
85
86 for (idx, step) in steps.iter().enumerate() {
87 if !crate::ast::guards_allow_any(&step.guards, &envs) {
88 continue;
89 }
90 match &step.kind {
91 StepKind::Workdir(path) => {
92 cwd = resolver
93 .resolve_workdir(&cwd, path)
94 .with_context(|| format!("step {}: WORKDIR {}", idx + 1, path))?;
95 }
96 StepKind::Workspace(target) => match target {
97 WorkspaceTarget::Snapshot => {
98 resolver.set_root(fs_root);
99 cwd = resolver.root().to_path_buf();
100 }
101 WorkspaceTarget::Local => {
102 resolver.set_root(build_context);
103 cwd = resolver.root().to_path_buf();
104 }
105 },
106 StepKind::Env { key, value } => {
107 envs.insert(key.clone(), value.clone());
108 }
109 StepKind::Run(cmd) => {
110 let mut command = shell_cmd(cmd);
111 command.current_dir(&cwd);
112 command.envs(envs.iter());
113 command.env("CARGO_TARGET_DIR", &cargo_target_dir);
115 run_cmd(&mut command).with_context(|| format!("step {}: RUN {}", idx + 1, cmd))?;
116 }
117 StepKind::Echo(msg) => {
118 let out = interpolate(msg, &envs);
119 println!("{}", out);
120 }
121 StepKind::RunBg(cmd) => {
122 let mut command = shell_cmd(cmd);
123 command.current_dir(&cwd);
124 command.envs(envs.iter());
125 command.env("CARGO_TARGET_DIR", &cargo_target_dir);
126 let child = command
127 .spawn()
128 .with_context(|| format!("step {}: RUN_BG {}", idx + 1, cmd))?;
129 bg_children.push(child);
130 }
131 StepKind::Copy { from, to } => {
132 let from_abs = resolver
133 .resolve_copy_source(from)
134 .with_context(|| format!("step {}: COPY {} {}", idx + 1, from, to))?;
135 let to_abs = resolver
136 .resolve_write(&cwd, to)
137 .with_context(|| format!("step {}: COPY {} {}", idx + 1, from, to))?;
138 copy_entry(&from_abs, &to_abs)
139 .with_context(|| format!("step {}: COPY {} {}", idx + 1, from, to))?;
140 }
141 StepKind::Symlink { from, to } => {
142 let to_abs = resolver
143 .resolve_write(&cwd, to)
144 .with_context(|| format!("step {}: SYMLINK {} {}", idx + 1, from, to))?;
145 if to_abs.exists() {
146 bail!("SYMLINK destination already exists: {}", to_abs.display());
147 }
148 let from_abs = resolver
149 .resolve_copy_source(from)
150 .with_context(|| format!("step {}: SYMLINK {} {}", idx + 1, from, to))?;
151 if from_abs == to_abs {
152 bail!(
153 "SYMLINK source resolves to the destination itself: {}",
154 from_abs.display()
155 );
156 }
157 #[cfg(unix)]
158 std::os::unix::fs::symlink(&from_abs, &to_abs)
159 .with_context(|| format!("step {}: SYMLINK {} {}", idx + 1, from, to))?;
160 #[cfg(all(windows, not(unix)))]
161 std::os::windows::fs::symlink_dir(&from_abs, &to_abs)
162 .with_context(|| format!("step {}: SYMLINK {} {}", idx + 1, from, to))?;
163 #[cfg(not(any(unix, windows)))]
164 copy_dir(&from_abs, &to_abs)?;
165 }
166 StepKind::Mkdir(path) => {
167 let target = resolver
168 .resolve_write(&cwd, path)
169 .with_context(|| format!("step {}: MKDIR {}", idx + 1, path))?;
170 fs::create_dir_all(&target)
171 .with_context(|| format!("failed to create dir {}", target.display()))?;
172 }
173 StepKind::Ls(path_opt) => {
174 let dir = if let Some(p) = path_opt.as_deref() {
175 resolver
176 .resolve_read(&cwd, p)
177 .with_context(|| format!("step {}: LS {}", idx + 1, p))?
178 } else {
179 cwd.clone()
180 };
181 let mut entries: Vec<_> = fs::read_dir(&dir)
182 .with_context(|| format!("failed to read dir {}", dir.display()))?
183 .collect::<Result<_, _>>()?;
184 entries.sort_by_key(|a| a.file_name());
185 println!("{}:", dir.display());
186 for entry in entries {
187 println!("{}", entry.file_name().to_string_lossy());
188 }
189 }
190 StepKind::Cat(path) => {
191 let target = resolver
192 .resolve_read(&cwd, path)
193 .with_context(|| format!("step {}: CAT {}", idx + 1, path))?;
194 let data = fs::read(&target)
195 .with_context(|| format!("failed to read {}", target.display()))?;
196 let mut out = io::stdout();
197 out.write_all(&data)
198 .with_context(|| format!("failed to write {} to stdout", target.display()))?;
199 out.flush().ok();
200 }
201 StepKind::Write { path, contents } => {
202 let target = resolver
203 .resolve_write(&cwd, path)
204 .with_context(|| format!("step {}: WRITE {}", idx + 1, path))?;
205 if let Some(parent) = target.parent() {
206 fs::create_dir_all(parent)
207 .with_context(|| format!("failed to create parent {}", parent.display()))?;
208 }
209 fs::write(&target, contents)
210 .with_context(|| format!("failed to write {}", target.display()))?;
211 }
212 StepKind::Exit(code) => {
213 for child in bg_children.iter_mut() {
214 if child.try_wait()?.is_none() {
215 let _ = child.kill();
216 let _ = child.wait();
217 }
218 }
219 bg_children.clear();
220 bail!("EXIT requested with code {}", code);
221 }
222 }
223
224 if let Some(status) = check_bg(&mut bg_children)? {
225 if status.success() {
226 return Ok(cwd);
227 } else {
228 bail!("RUN_BG exited with status {}", status);
229 }
230 }
231 }
232
233 if !bg_children.is_empty() {
234 let mut first = bg_children.remove(0);
235 let status = first.wait()?;
236 for child in bg_children.iter_mut() {
237 if child.try_wait()?.is_none() {
238 let _ = child.kill();
239 let _ = child.wait();
240 }
241 }
242 bg_children.clear();
243 if status.success() {
244 return Ok(cwd);
245 } else {
246 bail!("RUN_BG exited with status {}", status);
247 }
248 }
249
250 Ok(cwd)
251}
252
253fn run_cmd(cmd: &mut ProcessCommand) -> Result<()> {
254 let status = cmd
255 .status()
256 .with_context(|| format!("failed to run {:?}", cmd))?;
257 if !status.success() {
258 bail!("command {:?} failed with status {}", cmd, status);
259 }
260 Ok(())
261}
262
263fn copy_entry(src: &Path, dst: &Path) -> Result<()> {
264 if !src.exists() {
265 bail!("source missing: {}", src.display());
266 }
267 let meta = src.metadata()?;
268 if meta.is_dir() {
269 copy_dir(src, dst)?;
270 } else if meta.is_file() {
271 if let Some(parent) = dst.parent() {
272 fs::create_dir_all(parent)
273 .with_context(|| format!("creating dir {}", parent.display()))?;
274 }
275 fs::copy(src, dst)
276 .with_context(|| format!("copying {} to {}", src.display(), dst.display()))?;
277 } else {
278 bail!("unsupported file type: {}", src.display());
279 }
280 Ok(())
281}
282
283fn copy_dir(src: &Path, dst: &Path) -> Result<()> {
284 fs::create_dir_all(dst).with_context(|| format!("creating dir {}", dst.display()))?;
285 for entry in fs::read_dir(src)? {
286 let entry = entry?;
287 let file_type = entry.file_type()?;
288 let src_path = entry.path();
289 let dst_path = dst.join(entry.file_name());
290 if file_type.is_dir() {
291 copy_dir(&src_path, &dst_path)?;
292 } else if file_type.is_file() {
293 fs::copy(&src_path, &dst_path).with_context(|| {
294 format!("copying {} to {}", src_path.display(), dst_path.display())
295 })?;
296 } else {
297 bail!("unsupported file type: {}", src_path.display());
298 }
299 }
300 Ok(())
301}
302
303fn describe_dir(root: &Path, max_depth: usize, max_entries: usize) -> String {
304 fn helper(path: &Path, depth: usize, max_depth: usize, left: &mut usize, out: &mut String) {
305 if *left == 0 {
306 return;
307 }
308 let indent = " ".repeat(depth);
309 if depth > 0 {
310 out.push_str(&format!(
311 "{}{}\n",
312 indent,
313 path.file_name().unwrap_or_default().to_string_lossy()
314 ));
315 }
316 if depth >= max_depth {
317 return;
318 }
319 let entries = match fs::read_dir(path) {
320 Ok(e) => e,
321 Err(_) => return,
322 };
323 let mut names: Vec<_> = entries.filter_map(|e| e.ok()).collect();
324 names.sort_by_key(|a| a.file_name());
325 for entry in names {
326 if *left == 0 {
327 return;
328 }
329 *left -= 1;
330 let p = entry.path();
331 if p.is_dir() {
332 helper(&p, depth + 1, max_depth, left, out);
333 } else {
334 out.push_str(&format!(
335 "{} {}\n",
336 indent,
337 entry.file_name().to_string_lossy()
338 ));
339 }
340 }
341 }
342
343 let mut out = String::new();
344 let mut left = max_entries;
345 helper(root, 0, max_depth, &mut left, &mut out);
346 out
347}
348
349pub fn shell_program() -> String {
350 #[cfg(windows)]
351 {
352 std::env::var("COMSPEC").unwrap_or_else(|_| "cmd".to_string())
353 }
354
355 #[cfg(not(windows))]
356 {
357 std::env::var("SHELL").unwrap_or_else(|_| "sh".to_string())
358 }
359}
360
361fn shell_cmd(cmd: &str) -> ProcessCommand {
362 let program = shell_program();
363 let mut c = ProcessCommand::new(program);
364 if cfg!(windows) {
365 c.arg("/C").arg(cmd);
366 } else {
367 c.arg("-c").arg(cmd);
368 }
369 c
370}
371
372#[allow(clippy::while_let_on_iterator)]
373fn interpolate(template: &str, script_envs: &HashMap<String, String>) -> String {
374 let mut out = String::with_capacity(template.len());
375 let mut chars = template.chars().peekable();
376 while let Some(c) = chars.next() {
377 if c == '$' {
378 if let Some(&'{') = chars.peek() {
379 chars.next();
380 let mut name = String::new();
381 while let Some(&ch) = chars.peek() {
382 chars.next();
383 if ch == '}' {
384 break;
385 }
386 name.push(ch);
387 }
388 if !name.is_empty() {
389 let val = script_envs
390 .get(&name)
391 .cloned()
392 .or_else(|| std::env::var(&name).ok())
393 .unwrap_or_default();
394 out.push_str(&val);
395 }
396 } else {
397 let mut name = String::new();
398 while let Some(&ch) = chars.peek() {
399 if ch.is_ascii_alphanumeric() || ch == '_' {
400 name.push(ch);
401 chars.next();
402 } else {
403 break;
404 }
405 }
406 if !name.is_empty() {
407 let val = script_envs
408 .get(&name)
409 .cloned()
410 .or_else(|| std::env::var(&name).ok())
411 .unwrap_or_default();
412 out.push_str(&val);
413 } else {
414 out.push('$');
415 }
416 }
417 } else if c == '{' {
418 let mut name = String::new();
419 while let Some(ch) = chars.next() {
420 if ch == '}' {
421 break;
422 }
423 name.push(ch);
424 }
425 if !name.is_empty() {
426 let val = script_envs
427 .get(&name)
428 .cloned()
429 .or_else(|| std::env::var(&name).ok())
430 .unwrap_or_default();
431 out.push_str(&val);
432 }
433 } else {
434 out.push(c);
435 }
436 }
437 out
438}