1use std::collections::HashMap;
2use std::sync::Arc;
3
4pub use crate::commands::StepKind;
5
6#[derive(Copy, Clone, Debug, Eq, PartialEq)]
7pub enum Command {
8 InheritEnv,
9 Workdir,
10 Workspace,
11 Env,
12 Echo,
13 Run,
14 Copy,
15 WithIo,
16 CopyGit,
17 HashSha256,
18 Symlink,
19 Mkdir,
20 Ls,
21 Cwd,
22 Read,
23 ReadLine,
24 Write,
25 Append,
26 Expand,
27 AssertFile,
28 AssertDir,
29 AssertAbsent,
30 AssertStdout,
31 Exit,
32 Async,
33 Timeout,
34 Sleep,
35}
36
37pub const COMMANDS: &[Command] = &[
38 Command::InheritEnv,
39 Command::Workdir,
40 Command::Workspace,
41 Command::Env,
42 Command::Echo,
43 Command::Run,
44 Command::Copy,
45 Command::WithIo,
46 Command::CopyGit,
47 Command::HashSha256,
48 Command::Symlink,
49 Command::Mkdir,
50 Command::Ls,
51 Command::Cwd,
52 Command::Read,
53 Command::ReadLine,
54 Command::Write,
55 Command::Append,
56 Command::Expand,
57 Command::AssertFile,
58 Command::AssertDir,
59 Command::AssertAbsent,
60 Command::AssertStdout,
61 Command::Exit,
62 Command::Timeout,
63 Command::Sleep,
64];
65
66impl Command {
67 pub const fn as_str(self) -> &'static str {
68 match self {
69 Command::InheritEnv => "INHERIT_ENV",
70 Command::Workdir => "WORKDIR",
71 Command::Workspace => "WORKSPACE",
72 Command::Env => "ENV",
73 Command::Echo => "ECHO",
74 Command::Run => "RUN",
75 Command::Copy => "COPY",
76 Command::WithIo => "WITH_IO",
77 Command::CopyGit => "COPY_GIT",
78 Command::HashSha256 => "HASH_SHA256",
79 Command::Symlink => "SYMLINK",
80 Command::Mkdir => "MKDIR",
81 Command::Ls => "LS",
82 Command::Cwd => "CWD",
83 Command::Read => "READ",
84 Command::ReadLine => "READ_LINE",
85 Command::Write => "WRITE",
86 Command::Append => "APPEND",
87 Command::Expand => "EXPAND",
88 Command::AssertFile => "ASSERT_FILE",
89 Command::AssertDir => "ASSERT_DIR",
90 Command::AssertAbsent => "ASSERT_ABSENT",
91 Command::AssertStdout => "ASSERT_STDOUT",
92 Command::Exit => "EXIT",
93 Command::Async => "ASYNC",
94 Command::Timeout => "TIMEOUT",
95 Command::Sleep => "SLEEP",
96 }
97 }
98
99 pub const fn syntax(self) -> &'static str {
100 match self {
101 Command::InheritEnv => "INHERIT_ENV [KEY1, KEY2, ...]",
102 Command::Workdir => "WORKDIR <path>",
103 Command::Workspace => "WORKSPACE SNAPSHOT|LOCAL",
104 Command::Env => "ENV KEY=value",
105 Command::Echo => "ECHO <message>",
106 Command::Run => "RUN <command...>",
107 Command::Copy => "COPY [--from-current-workspace] <from> <to>",
108 Command::CopyGit => "COPY_GIT [--include-dirty] <rev> <src> <dst>",
109 Command::WithIo => "WITH_IO [bindings] [command | { block }]",
110 Command::HashSha256 => "HASH_SHA256 <path>",
111 Command::Symlink => "SYMLINK <from> <to>",
112 Command::Mkdir => "MKDIR <path>",
113 Command::Ls => "LS [<path>]",
114 Command::Cwd => "CWD",
115 Command::Read => "READ [<path>]",
116 Command::ReadLine => "READ_LINE $var",
117 Command::Write => "WRITE <path> [<contents>]",
118 Command::Append => "APPEND <path> [<contents>]",
119 Command::Expand => "EXPAND [<path>] [<KEY=val> ...]",
120 Command::AssertFile => "ASSERT_FILE [--hash <sha256>] <path> [<expected>]",
121 Command::AssertDir => "ASSERT_DIR <path>",
122 Command::AssertAbsent => "ASSERT_ABSENT <path>",
123 Command::AssertStdout => "ASSERT_STDOUT <substring>",
124 Command::Exit => "EXIT <code>",
125 Command::Async => "ASYNC <command...> | ASYNC { <commands> }",
126 Command::Timeout => {
127 "TIMEOUT <duration> <command...> | TIMEOUT <duration> { <commands> }"
128 }
129 Command::Sleep => "SLEEP <duration>",
130 }
131 }
132
133 pub const fn expects_inner_command(self) -> bool {
134 matches!(self, Command::WithIo | Command::Async | Command::Timeout)
135 }
136
137 pub fn parse(s: &str) -> Option<Self> {
138 match s {
139 "INHERIT_ENV" => Some(Command::InheritEnv),
140 "WORKDIR" => Some(Command::Workdir),
141 "WORKSPACE" => Some(Command::Workspace),
142 "ENV" => Some(Command::Env),
143 "ECHO" => Some(Command::Echo),
144 "RUN" => Some(Command::Run),
145 "COPY" => Some(Command::Copy),
146 "WITH_IO" => Some(Command::WithIo),
147 "COPY_GIT" => Some(Command::CopyGit),
148 "HASH_SHA256" => Some(Command::HashSha256),
149 "SYMLINK" => Some(Command::Symlink),
150 "MKDIR" => Some(Command::Mkdir),
151 "LS" => Some(Command::Ls),
152 "CWD" => Some(Command::Cwd),
153 "READ" => Some(Command::Read),
154 "READ_LINE" => Some(Command::ReadLine),
155 "WRITE" => Some(Command::Write),
156 "APPEND" => Some(Command::Append),
157 "EXPAND" => Some(Command::Expand),
158 "ASSERT_FILE" => Some(Command::AssertFile),
159 "ASSERT_DIR" => Some(Command::AssertDir),
160 "ASSERT_ABSENT" => Some(Command::AssertAbsent),
161 "ASSERT_STDOUT" => Some(Command::AssertStdout),
162 "EXIT" => Some(Command::Exit),
163 "ASYNC" => Some(Command::Async),
164 "TIMEOUT" => Some(Command::Timeout),
165 "SLEEP" => Some(Command::Sleep),
166 _ => None,
167 }
168 }
169}
170
171#[derive(Copy, Clone, Debug, Eq, PartialEq)]
172pub enum PlatformGuard {
173 Unix,
174 Windows,
175 Macos,
176 Linux,
177}
178
179#[derive(Debug, Clone, Eq, PartialEq)]
180pub enum Guard {
181 Platform { target: PlatformGuard },
182 EnvExists { key: String },
183 EnvEquals { key: String, value: String },
184 StaticBool { value: String },
185}
186
187#[derive(Debug, Clone, Eq, PartialEq)]
188pub enum GuardExpr {
189 Predicate(Guard),
190 All(Vec<GuardExpr>),
191 Or(Vec<GuardExpr>),
192 Not(Box<GuardExpr>),
193}
194
195impl GuardExpr {
196 pub fn all(exprs: Vec<GuardExpr>) -> GuardExpr {
197 let mut flat = Vec::new();
198 for expr in exprs {
199 match expr {
200 GuardExpr::All(children) => flat.extend(children),
201 other => flat.push(other),
202 }
203 }
204 match flat.len() {
205 0 => panic!("GuardExpr::all requires at least one expression"),
206 1 => flat.into_iter().next().unwrap(),
207 _ => GuardExpr::All(flat),
208 }
209 }
210
211 pub fn or(exprs: Vec<GuardExpr>) -> GuardExpr {
212 let mut flat = Vec::new();
213 for expr in exprs {
214 match expr {
215 GuardExpr::Or(children) => flat.extend(children),
216 other => flat.push(other),
217 }
218 }
219 match flat.len() {
220 0 => panic!("GuardExpr::or requires at least one expression"),
221 1 => flat.into_iter().next().unwrap(),
222 _ => GuardExpr::Or(flat),
223 }
224 }
225
226 pub fn invert(expr: GuardExpr) -> GuardExpr {
227 match expr {
228 GuardExpr::Not(inner) => *inner,
229 other => GuardExpr::Not(Box::new(other)),
230 }
231 }
232}
233
234impl std::ops::Not for GuardExpr {
235 type Output = GuardExpr;
236
237 fn not(self) -> GuardExpr {
238 match self {
239 GuardExpr::Not(inner) => *inner,
240 other => GuardExpr::Not(Box::new(other)),
241 }
242 }
243}
244
245impl From<Guard> for GuardExpr {
246 fn from(guard: Guard) -> Self {
247 GuardExpr::Predicate(guard)
248 }
249}
250
251#[derive(Debug, Clone, Eq, PartialEq)]
253pub enum Arg {
254 String(String, bool),
258 Expr(Expr),
260 Parts(Vec<ArgPart>),
264}
265
266#[derive(Debug, Clone, Eq, PartialEq)]
268pub enum ArgPart {
269 Text(String, bool),
272 Expr(Expr),
274}
275
276impl Arg {
277 pub fn as_str(&self) -> &str {
278 match self {
279 Arg::String(s, _) => s,
280 Arg::Expr(_) | Arg::Parts(_) => "",
283 }
284 }
285
286 pub fn render(&self) -> String {
290 match self {
291 Arg::String(s, _) => s.clone(),
292 Arg::Expr(e) => e.to_string(),
293 Arg::Parts(parts) => parts.iter().map(ArgPart::render).collect(),
294 }
295 }
296
297 pub fn is_quoted(&self) -> bool {
298 matches!(self, Arg::String(_, true))
299 }
300}
301
302impl ArgPart {
303 pub fn render(&self) -> String {
304 match self {
305 ArgPart::Text(s, _) => s.clone(),
306 ArgPart::Expr(e) => e.to_string(),
307 }
308 }
309}
310
311impl From<String> for Arg {
312 fn from(s: String) -> Self {
313 Arg::String(s, false)
314 }
315}
316
317impl From<&str> for Arg {
318 fn from(s: &str) -> Self {
319 Arg::String(s.to_string(), false)
320 }
321}
322
323impl std::fmt::Display for Arg {
324 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
325 match self {
326 Arg::String(s, _) => write!(f, "{}", s),
327 Arg::Expr(e) => write!(f, "{}", e),
328 Arg::Parts(parts) => {
329 for part in parts {
330 match part {
331 ArgPart::Text(s, _) => write!(f, "{}", s)?,
332 ArgPart::Expr(e) => write!(f, "{}", e)?,
333 }
334 }
335 Ok(())
336 }
337 }
338 }
339}
340
341impl AsRef<str> for Arg {
342 fn as_ref(&self) -> &str {
343 self.as_str()
344 }
345}
346
347impl PartialEq<str> for Arg {
348 fn eq(&self, other: &str) -> bool {
349 self.as_str() == other
350 }
351}
352
353impl PartialEq<&str> for Arg {
354 fn eq(&self, other: &&str) -> bool {
355 self.as_str() == *other
356 }
357}
358
359#[derive(Debug, Clone, Eq, PartialEq)]
360pub enum IoStream {
361 Stdin,
362 Stdout,
363 Stderr,
364}
365
366#[derive(Debug, Clone, Eq, PartialEq)]
367pub struct IoBinding {
368 pub stream: IoStream,
369 pub pipe: Option<String>,
370}
371
372#[derive(Debug, Clone, Eq, PartialEq)]
373pub enum Value {
374 String(String),
375 Int(i64),
376 List(Vec<Value>),
377 Map(std::collections::BTreeMap<String, Value>),
378 Bool(bool),
379 TaskHandle(u64),
382}
383
384#[derive(Debug, Clone, Copy, Eq, PartialEq)]
385pub enum CompareOp {
386 Eq,
387 Ne,
388}
389
390#[derive(Debug, Clone, Copy, Eq, PartialEq)]
391pub enum LogicalOp {
392 And,
393 Or,
394}
395
396#[derive(Debug, Clone, Eq, PartialEq)]
397pub enum Expr {
398 Literal(Value),
399 Var(String),
400 KeyPath {
401 base: String,
402 keys: Vec<String>,
403 },
404 List(Vec<Expr>),
405 Map(Vec<(String, Expr)>),
406 Call {
407 name: String,
408 args: Vec<Expr>,
409 },
410 Compare {
411 op: CompareOp,
412 left: Box<Expr>,
413 right: Box<Expr>,
414 },
415 Not(Box<Expr>),
416 Logical {
417 op: LogicalOp,
418 left: Box<Expr>,
419 right: Box<Expr>,
420 },
421}
422
423#[derive(Debug, Clone, Eq, PartialEq)]
424pub struct Step {
425 pub guard: Option<GuardExpr>,
426 pub kind: StepKind,
427 pub scope_enter: usize,
428 pub scope_exit: usize,
429}
430
431#[derive(Debug, Clone, Eq, PartialEq)]
432pub enum WorkspaceTarget {
433 Snapshot,
434 Local,
435}
436
437fn platform_matches(target: PlatformGuard) -> bool {
438 #[allow(clippy::disallowed_macros)]
439 match target {
440 PlatformGuard::Unix => cfg!(unix),
441 PlatformGuard::Windows => cfg!(windows),
442 PlatformGuard::Macos => cfg!(target_os = "macos"),
443 PlatformGuard::Linux => cfg!(target_os = "linux"),
444 }
445}
446
447pub trait EnvLookup {
448 fn get_env(&self, key: &str) -> Option<&str>;
449}
450
451impl EnvLookup for HashMap<String, String> {
452 fn get_env(&self, key: &str) -> Option<&str> {
453 self.get(key).map(|s| s.as_str())
454 }
455}
456
457impl EnvLookup for Arc<HashMap<String, String>> {
458 fn get_env(&self, key: &str) -> Option<&str> {
459 (**self).get_env(key)
460 }
461}
462
463pub fn guard_allows(guard: &Guard, env: &impl EnvLookup) -> bool {
464 match guard {
465 Guard::Platform { target } => platform_matches(*target),
466 Guard::EnvExists { key } => env.get_env(key).map(|v| !v.is_empty()).unwrap_or(false),
467 Guard::EnvEquals { key, value } => env
468 .get_env(key)
469 .map(|v| v == value.as_str())
470 .unwrap_or(false),
471 Guard::StaticBool { value } => value.parse::<bool>().unwrap_or(false),
472 }
473}
474
475pub fn guard_expr_allows(expr: &GuardExpr, env: &impl EnvLookup) -> bool {
476 match expr {
477 GuardExpr::Predicate(guard) => guard_allows(guard, env),
478 GuardExpr::All(children) => children.iter().all(|g| guard_expr_allows(g, env)),
479 GuardExpr::Or(children) => children.iter().any(|g| guard_expr_allows(g, env)),
480 GuardExpr::Not(child) => !guard_expr_allows(child, env),
481 }
482}
483
484pub fn guard_option_allows(expr: Option<&GuardExpr>, env: &impl EnvLookup) -> bool {
485 match expr {
486 Some(e) => guard_expr_allows(e, env),
487 None => true,
488 }
489}
490
491use std::fmt;
492
493impl fmt::Display for PlatformGuard {
494 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
495 match self {
496 PlatformGuard::Unix => write!(f, "unix"),
497 PlatformGuard::Windows => write!(f, "windows"),
498 PlatformGuard::Macos => write!(f, "macos"),
499 PlatformGuard::Linux => write!(f, "linux"),
500 }
501 }
502}
503
504impl fmt::Display for Guard {
505 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
506 match self {
507 Guard::Platform { target } => write!(f, "{}", target),
508 Guard::EnvExists { key } => write!(f, "env:{}", key),
509 Guard::EnvEquals { key, value } => write!(f, "eq(env:{}, {})", key, value),
510 Guard::StaticBool { value } => write!(f, "bool:{}", value),
511 }
512 }
513}
514
515impl fmt::Display for WorkspaceTarget {
516 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
517 match self {
518 WorkspaceTarget::Snapshot => write!(f, "SNAPSHOT"),
519 WorkspaceTarget::Local => write!(f, "LOCAL"),
520 }
521 }
522}
523
524impl fmt::Display for Value {
525 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
526 match self {
527 Value::String(s) => write!(f, "\"{}\"", s),
528 Value::Int(i) => write!(f, "{}", i),
529 Value::List(items) => {
530 write!(f, "[")?;
531 for (i, item) in items.iter().enumerate() {
532 if i > 0 {
533 write!(f, ", ")?;
534 }
535 write!(f, "{}", item)?;
536 }
537 write!(f, "]")
538 }
539 Value::Map(map) => {
540 write!(f, "{{")?;
541 for (i, (k, v)) in map.iter().enumerate() {
542 if i > 0 {
543 write!(f, ", ")?;
544 }
545 write!(f, "{}: {}", k, v)?;
546 }
547 write!(f, "}}")
548 }
549 Value::Bool(b) => write!(f, "{}", b),
550 Value::TaskHandle(id) => write!(f, "task#{}", id),
551 }
552 }
553}
554
555impl fmt::Display for Expr {
556 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
557 match self {
558 Expr::Literal(v) => write!(f, "{}", v),
559 Expr::Var(name) => write!(f, "${}", name),
560 Expr::KeyPath { base, keys } => {
561 write!(f, "${}", base)?;
562 for key in keys {
563 write!(f, ".{}", key)?;
564 }
565 Ok(())
566 }
567 Expr::Call { name, args } => {
568 write!(f, "{}(", name)?;
569 for (i, arg) in args.iter().enumerate() {
570 if i > 0 {
571 write!(f, ", ")?;
572 }
573 write!(f, "{}", arg)?;
574 }
575 write!(f, ")")
576 }
577 Expr::List(items) => {
578 write!(f, "[")?;
579 for (i, item) in items.iter().enumerate() {
580 if i > 0 {
581 write!(f, ", ")?;
582 }
583 write!(f, "{}", item)?;
584 }
585 write!(f, "]")
586 }
587 Expr::Map(entries) => {
588 write!(f, "{{")?;
589 for (i, (key, val)) in entries.iter().enumerate() {
590 if i > 0 {
591 write!(f, ", ")?;
592 }
593 write!(f, "\"{}\": {}", key, val)?;
594 }
595 write!(f, "}}")
596 }
597 Expr::Compare { op, left, right } => {
598 write!(f, "{} {} {}", left, op, right)
599 }
600 Expr::Not(inner) => {
601 match inner.as_ref() {
604 Expr::Compare { .. } => write!(f, "!({})", inner),
605 _ => write!(f, "!{}", inner),
606 }
607 }
608 Expr::Logical { op, left, right } => {
609 write!(f, "({} {} {})", left, op, right)
610 }
611 }
612 }
613}
614
615impl fmt::Display for CompareOp {
616 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
617 match self {
618 CompareOp::Eq => write!(f, "=="),
619 CompareOp::Ne => write!(f, "!="),
620 }
621 }
622}
623
624impl fmt::Display for LogicalOp {
625 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
626 match self {
627 LogicalOp::And => write!(f, "&&"),
628 LogicalOp::Or => write!(f, "||"),
629 }
630 }
631}
632
633enum GuardDisplayContext {
634 Root,
635 InAnyArg,
636 InNot,
637 InAll,
638}
639
640impl GuardExpr {
641 fn fmt_with_ctx(&self, f: &mut fmt::Formatter<'_>, ctx: GuardDisplayContext) -> fmt::Result {
642 match self {
643 GuardExpr::Predicate(guard) => write!(f, "{}", guard),
644 GuardExpr::All(children) => {
645 let wrap = matches!(
646 ctx,
647 GuardDisplayContext::InAnyArg | GuardDisplayContext::InNot
648 ) && children.len() > 1;
649 if wrap {
650 write!(f, "(")?;
651 }
652 for (i, child) in children.iter().enumerate() {
653 if i > 0 {
654 write!(f, ", ")?;
655 }
656 child.fmt_with_ctx(f, GuardDisplayContext::InAll)?;
657 }
658 if wrap {
659 write!(f, ")")?;
660 }
661 Ok(())
662 }
663 GuardExpr::Or(children) => {
664 write!(f, "any(")?;
665 for (i, child) in children.iter().enumerate() {
666 if i > 0 {
667 write!(f, ", ")?;
668 }
669 child.fmt_with_ctx(f, GuardDisplayContext::InAnyArg)?;
670 }
671 write!(f, ")")
672 }
673 GuardExpr::Not(child) => {
674 write!(f, "not(")?;
675 child.fmt_with_ctx(f, GuardDisplayContext::InNot)?;
676 write!(f, ")")
677 }
678 }
679 }
680}
681
682impl fmt::Display for GuardExpr {
683 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
684 self.fmt_with_ctx(f, GuardDisplayContext::Root)
685 }
686}
687
688impl fmt::Display for Step {
689 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
690 if let Some(expr) = &self.guard {
691 write!(f, "[{}] ", expr)?;
692 }
693 write!(f, "{}", self.kind)
694 }
695}