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}
261
262impl Arg {
263 pub fn as_str(&self) -> &str {
264 match self {
265 Arg::String(s, _) => s,
266 Arg::Expr(_) => "",
267 }
268 }
269
270 pub fn is_quoted(&self) -> bool {
271 matches!(self, Arg::String(_, true))
272 }
273}
274
275impl From<String> for Arg {
276 fn from(s: String) -> Self {
277 Arg::String(s, false)
278 }
279}
280
281impl From<&str> for Arg {
282 fn from(s: &str) -> Self {
283 Arg::String(s.to_string(), false)
284 }
285}
286
287impl std::fmt::Display for Arg {
288 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
289 match self {
290 Arg::String(s, _) => write!(f, "{}", s),
291 Arg::Expr(e) => write!(f, "{}", e),
292 }
293 }
294}
295
296impl AsRef<str> for Arg {
297 fn as_ref(&self) -> &str {
298 self.as_str()
299 }
300}
301
302impl PartialEq<str> for Arg {
303 fn eq(&self, other: &str) -> bool {
304 self.as_str() == other
305 }
306}
307
308impl PartialEq<&str> for Arg {
309 fn eq(&self, other: &&str) -> bool {
310 self.as_str() == *other
311 }
312}
313
314#[derive(Debug, Clone, Eq, PartialEq)]
315pub enum IoStream {
316 Stdin,
317 Stdout,
318 Stderr,
319}
320
321#[derive(Debug, Clone, Eq, PartialEq)]
322pub struct IoBinding {
323 pub stream: IoStream,
324 pub pipe: Option<String>,
325}
326
327#[derive(Debug, Clone, Eq, PartialEq)]
328pub enum Value {
329 String(String),
330 Int(i64),
331 List(Vec<Value>),
332 Map(std::collections::BTreeMap<String, Value>),
333 Bool(bool),
334 TaskHandle(u64),
337}
338
339#[derive(Debug, Clone, Copy, Eq, PartialEq)]
340pub enum CompareOp {
341 Eq,
342 Ne,
343}
344
345#[derive(Debug, Clone, Copy, Eq, PartialEq)]
346pub enum LogicalOp {
347 And,
348 Or,
349}
350
351#[derive(Debug, Clone, Eq, PartialEq)]
352pub enum Expr {
353 Literal(Value),
354 Var(String),
355 KeyPath {
356 base: String,
357 keys: Vec<String>,
358 },
359 List(Vec<Expr>),
360 Map(Vec<(String, Expr)>),
361 Call {
362 name: String,
363 args: Vec<Expr>,
364 },
365 Compare {
366 op: CompareOp,
367 left: Box<Expr>,
368 right: Box<Expr>,
369 },
370 Not(Box<Expr>),
371 Logical {
372 op: LogicalOp,
373 left: Box<Expr>,
374 right: Box<Expr>,
375 },
376}
377
378#[derive(Debug, Clone, Eq, PartialEq)]
379pub struct Step {
380 pub guard: Option<GuardExpr>,
381 pub kind: StepKind,
382 pub scope_enter: usize,
383 pub scope_exit: usize,
384}
385
386#[derive(Debug, Clone, Eq, PartialEq)]
387pub enum WorkspaceTarget {
388 Snapshot,
389 Local,
390}
391
392fn platform_matches(target: PlatformGuard) -> bool {
393 #[allow(clippy::disallowed_macros)]
394 match target {
395 PlatformGuard::Unix => cfg!(unix),
396 PlatformGuard::Windows => cfg!(windows),
397 PlatformGuard::Macos => cfg!(target_os = "macos"),
398 PlatformGuard::Linux => cfg!(target_os = "linux"),
399 }
400}
401
402pub trait EnvLookup {
403 fn get_env(&self, key: &str) -> Option<&str>;
404}
405
406impl EnvLookup for HashMap<String, String> {
407 fn get_env(&self, key: &str) -> Option<&str> {
408 self.get(key).map(|s| s.as_str())
409 }
410}
411
412impl EnvLookup for Arc<HashMap<String, String>> {
413 fn get_env(&self, key: &str) -> Option<&str> {
414 (**self).get_env(key)
415 }
416}
417
418pub fn guard_allows(guard: &Guard, env: &impl EnvLookup) -> bool {
419 match guard {
420 Guard::Platform { target } => platform_matches(*target),
421 Guard::EnvExists { key } => env.get_env(key).map(|v| !v.is_empty()).unwrap_or(false),
422 Guard::EnvEquals { key, value } => env
423 .get_env(key)
424 .map(|v| v == value.as_str())
425 .unwrap_or(false),
426 Guard::StaticBool { value } => value.parse::<bool>().unwrap_or(false),
427 }
428}
429
430pub fn guard_expr_allows(expr: &GuardExpr, env: &impl EnvLookup) -> bool {
431 match expr {
432 GuardExpr::Predicate(guard) => guard_allows(guard, env),
433 GuardExpr::All(children) => children.iter().all(|g| guard_expr_allows(g, env)),
434 GuardExpr::Or(children) => children.iter().any(|g| guard_expr_allows(g, env)),
435 GuardExpr::Not(child) => !guard_expr_allows(child, env),
436 }
437}
438
439pub fn guard_option_allows(expr: Option<&GuardExpr>, env: &impl EnvLookup) -> bool {
440 match expr {
441 Some(e) => guard_expr_allows(e, env),
442 None => true,
443 }
444}
445
446use std::fmt;
447
448impl fmt::Display for PlatformGuard {
449 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
450 match self {
451 PlatformGuard::Unix => write!(f, "unix"),
452 PlatformGuard::Windows => write!(f, "windows"),
453 PlatformGuard::Macos => write!(f, "macos"),
454 PlatformGuard::Linux => write!(f, "linux"),
455 }
456 }
457}
458
459impl fmt::Display for Guard {
460 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
461 match self {
462 Guard::Platform { target } => write!(f, "{}", target),
463 Guard::EnvExists { key } => write!(f, "env:{}", key),
464 Guard::EnvEquals { key, value } => write!(f, "eq(env:{}, {})", key, value),
465 Guard::StaticBool { value } => write!(f, "bool:{}", value),
466 }
467 }
468}
469
470impl fmt::Display for WorkspaceTarget {
471 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
472 match self {
473 WorkspaceTarget::Snapshot => write!(f, "SNAPSHOT"),
474 WorkspaceTarget::Local => write!(f, "LOCAL"),
475 }
476 }
477}
478
479impl fmt::Display for Value {
480 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
481 match self {
482 Value::String(s) => write!(f, "\"{}\"", s),
483 Value::Int(i) => write!(f, "{}", i),
484 Value::List(items) => {
485 write!(f, "[")?;
486 for (i, item) in items.iter().enumerate() {
487 if i > 0 {
488 write!(f, ", ")?;
489 }
490 write!(f, "{}", item)?;
491 }
492 write!(f, "]")
493 }
494 Value::Map(map) => {
495 write!(f, "{{")?;
496 for (i, (k, v)) in map.iter().enumerate() {
497 if i > 0 {
498 write!(f, ", ")?;
499 }
500 write!(f, "{}: {}", k, v)?;
501 }
502 write!(f, "}}")
503 }
504 Value::Bool(b) => write!(f, "{}", b),
505 Value::TaskHandle(id) => write!(f, "task#{}", id),
506 }
507 }
508}
509
510impl fmt::Display for Expr {
511 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
512 match self {
513 Expr::Literal(v) => write!(f, "{}", v),
514 Expr::Var(name) => write!(f, "${}", name),
515 Expr::KeyPath { base, keys } => {
516 write!(f, "${}", base)?;
517 for key in keys {
518 write!(f, ".{}", key)?;
519 }
520 Ok(())
521 }
522 Expr::Call { name, args } => {
523 write!(f, "{}(", name)?;
524 for (i, arg) in args.iter().enumerate() {
525 if i > 0 {
526 write!(f, ", ")?;
527 }
528 write!(f, "{}", arg)?;
529 }
530 write!(f, ")")
531 }
532 Expr::List(items) => {
533 write!(f, "[")?;
534 for (i, item) in items.iter().enumerate() {
535 if i > 0 {
536 write!(f, ", ")?;
537 }
538 write!(f, "{}", item)?;
539 }
540 write!(f, "]")
541 }
542 Expr::Map(entries) => {
543 write!(f, "{{")?;
544 for (i, (key, val)) in entries.iter().enumerate() {
545 if i > 0 {
546 write!(f, ", ")?;
547 }
548 write!(f, "\"{}\": {}", key, val)?;
549 }
550 write!(f, "}}")
551 }
552 Expr::Compare { op, left, right } => {
553 write!(f, "{} {} {}", left, op, right)
554 }
555 Expr::Not(inner) => {
556 match inner.as_ref() {
559 Expr::Compare { .. } => write!(f, "!({})", inner),
560 _ => write!(f, "!{}", inner),
561 }
562 }
563 Expr::Logical { op, left, right } => {
564 write!(f, "({} {} {})", left, op, right)
565 }
566 }
567 }
568}
569
570impl fmt::Display for CompareOp {
571 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
572 match self {
573 CompareOp::Eq => write!(f, "=="),
574 CompareOp::Ne => write!(f, "!="),
575 }
576 }
577}
578
579impl fmt::Display for LogicalOp {
580 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
581 match self {
582 LogicalOp::And => write!(f, "&&"),
583 LogicalOp::Or => write!(f, "||"),
584 }
585 }
586}
587
588enum GuardDisplayContext {
589 Root,
590 InAnyArg,
591 InNot,
592 InAll,
593}
594
595impl GuardExpr {
596 fn fmt_with_ctx(&self, f: &mut fmt::Formatter<'_>, ctx: GuardDisplayContext) -> fmt::Result {
597 match self {
598 GuardExpr::Predicate(guard) => write!(f, "{}", guard),
599 GuardExpr::All(children) => {
600 let wrap = matches!(
601 ctx,
602 GuardDisplayContext::InAnyArg | GuardDisplayContext::InNot
603 ) && children.len() > 1;
604 if wrap {
605 write!(f, "(")?;
606 }
607 for (i, child) in children.iter().enumerate() {
608 if i > 0 {
609 write!(f, ", ")?;
610 }
611 child.fmt_with_ctx(f, GuardDisplayContext::InAll)?;
612 }
613 if wrap {
614 write!(f, ")")?;
615 }
616 Ok(())
617 }
618 GuardExpr::Or(children) => {
619 write!(f, "any(")?;
620 for (i, child) in children.iter().enumerate() {
621 if i > 0 {
622 write!(f, ", ")?;
623 }
624 child.fmt_with_ctx(f, GuardDisplayContext::InAnyArg)?;
625 }
626 write!(f, ")")
627 }
628 GuardExpr::Not(child) => {
629 write!(f, "not(")?;
630 child.fmt_with_ctx(f, GuardDisplayContext::InNot)?;
631 write!(f, ")")
632 }
633 }
634 }
635}
636
637impl fmt::Display for GuardExpr {
638 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
639 self.fmt_with_ctx(f, GuardDisplayContext::Root)
640 }
641}
642
643impl fmt::Display for Step {
644 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
645 if let Some(expr) = &self.guard {
646 write!(f, "[{}] ", expr)?;
647 }
648 write!(f, "{}", self.kind)
649 }
650}