e0918b88 Andy McCall 1 month ago
1
2
3
4
5
use serde:: { Deserialize, Serialize} ;
6
use std:: collections:: { BTreeMap, BTreeSet} ;
7
use std:: fmt;
8
9
10
pub const PIPELINE_PATH : & str = " .flow/pipeline.kdl" ;
11
12
# [ derive ( Clone, Debug, PartialEq, Eq, Serialize, Deserialize ) ]
13
pub struct Pipeline {
14
pub schema_version : u32 ,
15
16
pub sandboxes : BTreeMap< String, Sandbox> ,
17
18
pub jobs : BTreeMap< String, Job> ,
bf7bb1b8 Andy McCall 1 month ago ↶
19
20
21
#[serde(default, skip_serializing_if = "Option : : is_none")]
22
pub on: Option< On> ,
23
}
24
25
26
# [ derive ( Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize ) ]
27
pub struct On {
28
29
#[serde(default, skip_serializing_if = "Option : : is_none")]
30
pub push: Option< Vec< String> > ,
31
32
#[serde(default, skip_serializing_if = "Option : : is_none")]
33
pub tag: Option< Vec< String> > ,
34
35
#[serde(default, skip_serializing_if = "std : : ops:: Not :: not")]
36
pub manual: bool ,
37
38
#[serde(default, skip_serializing_if = "Vec : : is_empty")]
39
pub schedule: Vec< String> ,
e0918b88 Andy McCall 1 month ago
40
}
41
bf7bb1b8 Andy McCall 1 month ago ↶
42
43
# [ derive ( Clone, Copy, Debug, PartialEq, Eq ) ]
44
pub enum Event <'a> {
45
46
Push( & 'a str ) ,
47
48
Tag( & 'a str ) ,
49
50
Manual,
51
52
Schedule,
53
}
54
55
impl On {
56
57
pub fn default_on ( ) -> On {
58
On { push: Some ( vec! [ ] ) , tag: None , manual: true , schedule: vec! [ ] }
59
}
60
61
pub fn matches ( & self , event : Event< '_ > ) -> bool {
62
let any = | globs : & Option< Vec< String> > , name : & str | match globs {
63
None => false ,
64
Some ( g) if g.is_empty ( ) => true ,
65
Some ( g) => g.iter ( ) .any ( | p | globset:: Glob:: new( p) .is_ok_and ( | p | p.compile_matcher ( ) .is_match ( name) ) ) ,
66
} ;
67
match event {
68
Event:: Push( b) => any ( & self .push, b) ,
69
Event:: Tag( t) => any ( & self .tag, t) ,
70
Event:: Manual => self .manual,
71
Event:: Schedule => ! self .schedule.is_empty ( ) ,
72
}
73
}
74
}
75
76
impl Pipeline {
77
78
pub fn on_of ( & self , job : & Job) -> On {
79
job.on.clone ( ) .or_else ( | | self .on.clone ( ) ) .unwrap_or_else ( On:: default_on)
80
}
81
}
82
e0918b88 Andy McCall 1 month ago
83
# [ derive ( Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize ) ]
84
# [ serde ( rename_all = " lowercase" ) ]
85
pub enum Runtime {
86
87
Wasm,
88
89
Syscall,
90
91
Microvm,
92
}
93
94
impl Runtime {
95
pub fn as_str ( self ) -> & 'static str {
96
match self {
97
Runtime:: Wasm => " wasm" ,
98
Runtime:: Syscall => " syscall" ,
99
Runtime:: Microvm => " microvm" ,
100
}
101
}
102
}
103
104
# [ derive ( Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize ) ]
105
# [ serde ( rename_all = " lowercase" ) ]
106
pub enum Network {
107
Isolated,
108
Loopback,
109
Egress,
110
}
111
112
impl Network {
113
pub fn as_str ( self ) -> & 'static str {
114
match self {
115
Network:: Isolated => " isolated" ,
116
Network:: Loopback => " loopback" ,
117
Network:: Egress => " egress" ,
118
}
119
}
120
}
121
122
# [ derive ( Clone, Debug, PartialEq, Eq, Serialize, Deserialize ) ]
123
pub struct Sandbox {
124
pub name : String,
125
pub runtime : Runtime,
126
127
pub rootfs : String,
128
pub memory_mib : u32 ,
129
pub cpus : u32 ,
130
pub network : Network,
a72abeee Andy McCall 1 month ago ↶
131
132
133
#[serde(default, skip_serializing_if = "BTreeMap : : is_empty")]
134
pub env: BTreeMap< String, String> ,
e576e086 Andy McCall 1 month ago ↶
135
136
#[serde(default, skip_serializing_if = "Vec : : is_empty")]
137
pub runs_on: Vec< String> ,
02b127d2 Andy McCall 1 month ago ↶
138
139
140
#[serde(default, skip_serializing_if = "Option : : is_none")]
141
pub timeout_s: Option< u64 > ,
e0918b88 Andy McCall 1 month ago
142
}
143
144
# [ derive ( Clone, Debug, PartialEq, Eq, Serialize, Deserialize ) ]
145
pub struct Job {
146
pub name : String,
147
pub sandbox : String,
148
149
pub scope : Vec< String> ,
150
151
pub depends_on : Vec< String> ,
152
pub env : BTreeMap< String, String> ,
153
154
pub exec : Vec< String> ,
fc10d9e8 Andy McCall 1 month ago ↶
155
156
#[serde(default, skip_serializing_if = "Vec : : is_empty")]
157
pub mounts: Vec< Mount> ,
a67b97e9 Andy McCall 1 month ago ↶
158
159
#[serde(default, skip_serializing_if = "Vec : : is_empty")]
160
pub outputs: Vec< Output> ,
e576e086 Andy McCall 1 month ago ↶
161
162
163
#[serde(default, skip_serializing_if = "Vec : : is_empty")]
164
pub runs_on: Vec< String> ,
02b127d2 Andy McCall 1 month ago ↶
165
166
167
#[serde(default, skip_serializing_if = "Option : : is_none")]
168
pub timeout_s: Option< u64 > ,
bf7bb1b8 Andy McCall 1 month ago ↶
169
170
#[serde(default, skip_serializing_if = "Option : : is_none")]
171
pub on: Option< On> ,
e87c1c13 Andy McCall 20 days ago ↶
172
173
174
175
#[serde(default, skip_serializing_if = "Option : : is_none")]
176
pub priority: Option< String> ,
fc10d9e8 Andy McCall 1 month ago ↶
177
}
178
c4285535 Andy McCall 1 month ago ↶
179
impl Job {
a72abeee Andy McCall 1 month ago ↶
180
181
pub fn in_sandbox ( & self , sandbox : & Sandbox) -> Job {
182
let mut job = self .clone ( ) ;
183
for ( k, v) in & sandbox.env {
184
job.env.entry ( k.clone ( ) ) .or_insert_with ( | | v.clone ( ) ) ;
185
}
e576e086 Andy McCall 1 month ago ↶
186
job.runs_on.extend ( sandbox.runs_on.iter ( ) .cloned ( ) ) ;
187
job.runs_on.sort ( ) ;
188
job.runs_on.dedup ( ) ;
02b127d2 Andy McCall 1 month ago ↶
189
job.timeout_s = job.timeout_s.or ( sandbox.timeout_s) ;
a72abeee Andy McCall 1 month ago ↶
190
job
191
}
192
c4285535 Andy McCall 1 month ago ↶
193
194
195
196
pub fn with_flow_env ( & self , vars : & [( String , String ) ]) -> Job {
197
let mut job = self .clone ( ) ;
198
for ( k, v) in vars {
199
job.env.entry ( k.clone ( ) ) .or_insert_with ( | | v.clone ( ) ) ;
200
}
201
job
202
}
203
}
204
fc10d9e8 Andy McCall 1 month ago ↶
205
206
207
# [ derive ( Clone, Debug, PartialEq, Eq, Serialize, Deserialize ) ]
208
pub struct Mount {
209
pub source : MountSource,
210
211
pub target : String,
212
pub size_mib : u32 ,
213
}
214
215
# [ derive ( Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize ) ]
216
# [ serde ( rename_all = " lowercase" ) ]
217
pub enum MountSource {
218
219
Tmpfs,
220
221
Disk,
222
}
223
224
impl MountSource {
225
pub fn as_str ( self ) -> & 'static str {
226
match self {
227
MountSource:: Tmpfs => " ephemeral://tmpfs" ,
228
MountSource:: Disk => " ephemeral://disk" ,
229
}
230
}
a67b97e9 Andy McCall 1 month ago ↶
231
}
232
233
234
235
# [ derive ( Clone, Debug, PartialEq, Eq, Serialize, Deserialize ) ]
236
pub struct Output {
237
238
pub name : String,
239
240
pub path : String,
241
}
242
243
244
pub const MAX_OUTPUTS : usize = 16 ;
245
246
247
248
fn output_path_ok ( p : & str ) -> bool {
249
p.strip_prefix ( " /src/" ) .is_some_and ( | rest | {
250
! rest.is_empty ( )
251
& & rest.split ( ' /' ) .all ( | c | ! c.is_empty ( ) & & c ! = " ." & & c ! = " .." )
252
& & p.bytes ( ) .all ( | b | b.is_ascii_graphic ( ) & & b ! = b ' "' & & b ! = b ' \\ ' )
253
} )
fc10d9e8 Andy McCall 1 month ago ↶
254
}
255
256
257
pub const MAX_MOUNTS : usize = 4 ;
258
259
pub const MIN_MOUNT_MIB : u64 = 16 ;
260
pub const MAX_MOUNT_MIB : u64 = 64 * 1024 ;
261
262
263
fn mount_target_ok ( t : & str ) -> bool {
264
let under = t.strip_prefix ( " /src/" ) .or_else ( | | t.strip_prefix ( " /tmp/" ) ) ;
265
under.is_some_and ( | rest | ! rest.is_empty ( ) & & rest.split ( ' /' ) .all ( | c | ! c.is_empty ( ) & & c ! = " ." & & c ! = " .." ) )
e0918b88 Andy McCall 1 month ago
266
}
267
268
269
# [ derive ( Clone, Debug, PartialEq, Eq, Serialize, Deserialize ) ]
270
pub struct Problem {
271
pub line : usize ,
272
pub message : String,
273
}
274
275
impl fmt:: Display for Problem {
276
fn fmt ( & self , f : & mut fmt:: Formatter< '_ > ) -> fmt:: Result {
277
write! ( f, " line {} : {} " , self .line, self .message)
278
}
279
}
280
281
282
# [ derive ( Clone, Debug, PartialEq, Eq, Serialize, Deserialize, thiserror::Error ) ]
283
pub struct Problems ( pub Vec< Problem> ) ;
284
285
impl fmt:: Display for Problems {
286
fn fmt ( & self , f : & mut fmt:: Formatter< '_ > ) -> fmt:: Result {
287
let lines: Vec< String> = self .0. iter ( ) .map ( | p | p.to_string ( ) ) .collect ( ) ;
288
f.write_str ( & lines.join ( " \n " ) )
289
}
290
}
291
292
const MIN_MEMORY_MIB : u64 = 64 ;
293
const MAX_MEMORY_MIB : u64 = 16 * 1024 ;
294
const MAX_CPUS : i128 = 16 ;
295
296
297
const LATER_TOP : & [ & str ] = & [ " publish" , " secret" ] ;
a67b97e9 Andy McCall 1 month ago ↶
298
const LATER_JOB : & [ & str ] = & [ " secret" , " attest" ] ;
e0918b88 Andy McCall 1 month ago
299
300
struct Ctx < 'a > {
301
src : & 'a str ,
302
problems : Vec< Problem> ,
303
}
304
305
impl Ctx < '_ > {
306
fn line ( & self , span : SourceSpan) -> usize {
307
let at = span.offset ( ) .min ( self .src.len ( ) ) ;
308
self .src[ .. at] .bytes ( ) .filter ( | b | * b = = b ' \n ' ) .count ( ) + 1
309
}
310
311
fn err ( & mut self , span : SourceSpan, message : impl Into< String> ) {
312
let line = self .line ( span) ;
313
self .problems.push ( Problem { line, message: message.into ( ) } ) ;
314
}
315
316
317
fn strings ( & mut self , node : & KdlNode) -> Vec< String> {
318
let mut out = Vec :: new( ) ;
319
for e in node.entries ( ) {
320
if let Some ( k) = e.name ( ) {
321
self .err ( e.span ( ) , format! ( " `{} ` takes no property `{} `" , node.name ( ) .value ( ) , k.value ( ) ) ) ;
322
continue ;
323
}
324
match e.value ( ) {
325
KdlValue:: String( s) => out.push ( s.clone ( ) ) ,
326
other => self .err ( e.span ( ) , format! ( " `{} ` takes strings, not {} " , node.name ( ) .value ( ) , kind_of ( other) ) ) ,
327
}
328
}
329
out
330
}
331
332
333
fn one_string ( & mut self , node : & KdlNode) -> Option< String> {
334
let v = self .strings ( node) ;
335
if v.len ( ) ! = 1 {
336
self .err ( node.span ( ) , format! ( " `{} ` takes one string" , node.name ( ) .value ( ) ) ) ;
337
return None ;
338
}
339
v.into_iter ( ) .next ( )
340
}
341
342
fn one_integer ( & mut self , node : & KdlNode) -> Option< i128> {
343
match node.entries ( ) {
344
[ e] if e.name ( ) .is_none ( ) => match e.value ( ) {
345
KdlValue:: Integer( n) => Some ( * n) ,
346
other => {
347
self .err ( e.span ( ) , format! ( " `{} ` takes an integer, not {} " , node.name ( ) .value ( ) , kind_of ( other) ) ) ;
348
None
349
}
350
} ,
351
_ => {
352
self .err ( node.span ( ) , format! ( " `{} ` takes one integer" , node.name ( ) .value ( ) ) ) ;
353
None
354
}
355
}
356
}
357
358
fn no_children ( & mut self , node : & KdlNode) {
359
if node.children ( ) .is_some ( ) {
360
self .err ( node.span ( ) , format! ( " `{} ` takes no block" , node.name ( ) .value ( ) ) ) ;
361
}
362
}
363
364
fn name_ok ( & mut self , span : SourceSpan, what : & str , name : & str ) -> bool {
365
let ok = ! name.is_empty ( ) & & name.len ( ) < = 64 & & name.bytes ( ) .all ( | b | b.is_ascii_alphanumeric ( ) | | b " ._-" .contains ( & b) ) ;
366
if ! ok {
367
self .err ( span, format! ( " {what} name {name:?} must be 1 to 64 of A-Z a-z 0-9 . _ -" ) ) ;
368
}
369
ok
370
}
371
}
372
373
use kdl:: { KdlDocument, KdlNode, KdlValue} ;
374
use miette:: SourceSpan;
375
376
fn kind_of ( v : & KdlValue) -> & 'static str {
377
match v {
378
KdlValue:: String( _ ) => " a string" ,
379
KdlValue:: Integer( _ ) => " an integer" ,
380
KdlValue:: Float( _ ) => " a number" ,
381
KdlValue:: Bool( _ ) => " a boolean" ,
382
KdlValue:: Null => " null" ,
383
}
384
}
385
386
387
fn parse_memory ( s : & str ) -> Option< u64 > {
388
let ( num, unit) = s.split_at ( s.find ( | c : char | ! c.is_ascii_digit ( ) ) ? ) ;
389
let n: u64 = num.parse ( ) .ok ( ) ? ;
390
let mib = match unit {
391
" KiB" => n.is_multiple_of ( 1024 ) .then_some ( n / 1024 ) ? ,
392
" MiB" => n,
393
" GiB" => n.checked_mul ( 1024 ) ? ,
394
_ => return None ,
395
} ;
396
Some ( mib)
397
}
398
b647ab72 Andy McCall 1 month ago ↶
399
400
401
fn foreign_escape_line ( src : & str ) -> Option< usize > {
402
src.lines ( )
403
.position ( | l | {
404
let b = l.as_bytes ( ) ;
405
b.windows ( 2 ) .enumerate ( ) .any ( | ( i , w ) | {
406
w[ 0 ] = = b ' \\ '
407
& & ( i = = 0 | | b[ i - 1 ] ! = b ' \\ ' )
408
& & ( w[ 1 ] = = b ' e' | | w[ 1 ] = = b ' x' | | ( w[ 1 ] = = b ' 0' & & b.get ( i + 2 ) .is_some_and ( u8 :: is_ascii_digit) ) )
409
} )
410
} )
411
.map ( | i | i + 1 )
412
}
413
e0918b88 Andy McCall 1 month ago
414
impl Pipeline {
415
416
pub fn parse ( src : & str ) -> Result< Pipeline, Problems> {
417
let doc = match KdlDocument:: parse_v2( src) {
418
Ok ( d) => d,
419
Err ( e) => {
420
let mut ctx = Ctx { src, problems: Vec :: new( ) } ;
421
for d in & e.diagnostics {
422
let msg = d.message.clone ( ) .or_else ( | | d.label.clone ( ) ) .unwrap_or_else ( | | " not valid KDL 2.0" .into ( ) ) ;
423
ctx.err ( d.span, msg) ;
424
}
425
if ctx.problems.is_empty ( ) {
426
ctx.problems.push ( Problem { line: 1 , message: " not valid KDL 2.0" .into ( ) } ) ;
b647ab72 Andy McCall 1 month ago ↶
427
}
428
if let Some ( line) = foreign_escape_line ( src) {
429
ctx.problems.push ( Problem {
430
line,
431
message: " KDL strings have no \\ 033, \\ e or \\ x escapes: write ESC (for ANSI colours) as \\ u{1b}" .into ( ) ,
432
} ) ;
e0918b88 Andy McCall 1 month ago
433
}
434
return Err ( Problems( ctx.problems) ) ;
435
}
436
} ;
437
let mut ctx = Ctx { src, problems: Vec :: new( ) } ;
438
let mut schema = None ;
439
let mut sandboxes = BTreeMap:: new( ) ;
440
let mut jobs: BTreeMap< String, ( Job, SourceSpan) > = BTreeMap:: new( ) ;
bf7bb1b8 Andy McCall 1 month ago ↶
441
let mut on = None ;
e87c1c13 Andy McCall 20 days ago ↶
442
let mut priority = None ;
e0918b88 Andy McCall 1 month ago
443
for node in doc.nodes ( ) {
444
match node.name ( ) .value ( ) {
e87c1c13 Andy McCall 20 days ago ↶
445
" priority" => {
446
if priority.is_some ( ) {
447
ctx.err ( node.span ( ) , " `priority` is given twice" ) ;
448
}
449
priority = parse_priority ( & mut ctx, node) ;
450
}
bf7bb1b8 Andy McCall 1 month ago ↶
451
" on" => {
452
if on.is_some ( ) {
453
ctx.err ( node.span ( ) , " `on` is given twice" ) ;
454
}
455
on = Some ( parse_on ( & mut ctx, node) ) ;
456
}
e0918b88 Andy McCall 1 month ago
457
" schema-version" => {
458
ctx.no_children ( node) ;
459
match ctx.one_integer ( node) {
460
Some ( 1 ) => schema = Some ( 1 ) ,
461
Some ( n) => ctx.err ( node.span ( ) , format! ( " schema-version {n} is not supported; this runner reads 1" ) ) ,
462
None => { }
463
}
464
}
465
" sandbox" => {
466
if let Some ( sb) = parse_sandbox ( & mut ctx, node) {
467
if sandboxes.contains_key ( & sb.name) {
468
ctx.err ( node.span ( ) , format! ( " sandbox {:?} is declared twice" , sb.name) ) ;
469
} else {
470
sandboxes.insert ( sb.name.clone ( ) , sb) ;
471
}
472
}
473
}
474
" job" => {
475
if let Some ( job) = parse_job ( & mut ctx, node) {
476
if jobs.contains_key ( & job.name) {
477
ctx.err ( node.span ( ) , format! ( " job {:?} is declared twice" , job.name) ) ;
478
} else {
479
jobs.insert ( job.name.clone ( ) , ( job, node.span ( ) ) ) ;
480
}
481
}
482
}
483
n if LATER_TOP .contains ( & n) => ctx.err ( node.span ( ) , format! ( " `{n} ` is not in M0 yet" ) ) ,
e87c1c13 Andy McCall 20 days ago ↶
484
n => ctx.err ( node.span ( ) , format! ( " unknown node `{n} `; expected schema-version, on, priority, sandbox or job" ) ) ,
e0918b88 Andy McCall 1 month ago
485
}
486
}
487
if schema.is_none ( ) & & ! ctx.problems.iter ( ) .any ( | p | p.message.contains ( " schema-version" ) ) {
488
ctx.problems.push ( Problem { line: 1 , message: " `schema-version 1` is required" .into ( ) } ) ;
489
}
490
if jobs.is_empty ( ) & & ctx.problems.is_empty ( ) {
491
ctx.problems.push ( Problem { line: 1 , message: " a pipeline needs at least one job" .into ( ) } ) ;
492
}
493
494
for ( job, span) in jobs.values ( ) {
495
if ! job.sandbox.is_empty ( ) & & ! sandboxes.contains_key ( & job.sandbox) {
496
ctx.err ( * span, format! ( " job {:?} uses sandbox {:?} , which is not declared" , job.name, job.sandbox) ) ;
497
}
498
for d in & job.depends_on {
499
if ! jobs.contains_key ( d) {
500
ctx.err ( * span, format! ( " job {:?} depends on {:?} , which is not declared" , job.name, d) ) ;
501
} else if d = = & job.name {
502
ctx.err ( * span, format! ( " job {:?} depends on itself" , job.name) ) ;
503
}
504
}
505
}
506
let graph: BTreeMap< & str , Vec< & str > > =
507
jobs.values ( ) .map ( | ( j , _) | ( j.name.as_str ( ) , j.depends_on.iter ( ) .map ( String :: as_str) .collect ( ) ) ) .collect ( ) ;
508
if let Some ( cycle) = find_cycle ( & graph) {
509
let span = jobs[ cycle[ 0 ] ] .1 ;
510
ctx.err ( span, format! ( " jobs depend on each other in a cycle: {} " , cycle.join ( " → " ) ) ) ;
511
}
512
if ! ctx.problems.is_empty ( ) {
513
ctx.problems.sort_by_key ( | p | p.line ) ;
514
return Err ( Problems( ctx.problems) ) ;
515
}
e87c1c13 Andy McCall 20 days ago ↶
516
517
let jobs = jobs
518
.into_iter ( )
519
.map ( | ( k , (mut j , _) ) | {
520
if j.priority.is_none ( ) {
521
j.priority.clone_from ( & priority) ;
522
}
523
( k, j)
524
} )
525
.collect ( ) ;
526
Ok ( Pipeline { schema_version: 1 , sandboxes, jobs, on } )
e0918b88 Andy McCall 1 month ago
527
}
528
529
530
pub fn order ( & self ) -> Vec< & Job> {
531
let mut done: BTreeSet< & str > = BTreeSet:: new( ) ;
532
let mut out = Vec :: with_capacity( self .jobs.len ( ) ) ;
533
while out.len ( ) < self .jobs.len ( ) {
534
let before = out.len ( ) ;
535
for j in self .jobs.values ( ) {
536
if ! done.contains ( j.name.as_str ( ) ) & & j.depends_on.iter ( ) .all ( | d | done.contains ( d.as_str ( ) ) ) {
537
out.push ( j) ;
538
}
539
}
540
for j in & out[ before.. ] {
541
done.insert ( j.name.as_str ( ) ) ;
542
}
543
if out.len ( ) = = before {
544
break ;
545
}
546
}
547
out
548
}
549
550
551
pub fn depths ( & self ) -> BTreeMap< & str , usize > {
552
let mut depth: BTreeMap< & str , usize > = BTreeMap:: new( ) ;
553
for j in self .order ( ) {
554
let d = j.depends_on.iter ( ) .map ( | x | depth.get ( x.as_str ( ) ) .copied ( ) .unwrap_or ( 0 ) + 1 ) .max ( ) .unwrap_or ( 0 ) ;
555
depth.insert ( j.name.as_str ( ) , d) ;
556
}
557
depth
558
}
559
}
560
561
fn find_cycle < 'a > ( graph : & BTreeMap< & 'a str , Vec< & 'a str > > ) -> Option< Vec< & 'a str > > {
562
# [ derive ( Clone, Copy, PartialEq ) ]
563
enum Mark {
564
Visiting,
565
Done,
566
}
567
fn visit < 'a > (
568
n : & 'a str ,
569
graph : & BTreeMap< & 'a str , Vec< & 'a str > > ,
570
marks : & mut BTreeMap< & 'a str , Mark> ,
571
stack : & mut Vec< & 'a str > ,
572
) -> Option< Vec< & 'a str > > {
573
match marks.get ( n) {
574
Some ( Mark:: Done) => return None ,
575
Some ( Mark:: Visiting) => {
576
let at = stack.iter ( ) .position ( | s | * s = = n) .unwrap_or ( 0 ) ;
577
let mut c = stack[ at.. ] .to_vec ( ) ;
578
c.push ( n) ;
579
return Some ( c) ;
580
}
581
None => { }
582
}
583
marks.insert ( n, Mark:: Visiting) ;
584
stack.push ( n) ;
585
for d in graph.get ( n) .into_iter ( ) .flatten ( ) {
586
if graph.contains_key ( d) & & * d ! = n {
587
if let Some ( c) = visit ( d, graph, marks, stack) {
588
return Some ( c) ;
589
}
590
}
591
}
592
stack.pop ( ) ;
593
marks.insert ( n, Mark:: Done) ;
594
None
595
}
596
let mut marks = BTreeMap:: new( ) ;
597
for n in graph.keys ( ) {
598
let mut stack = Vec :: new( ) ;
599
if let Some ( c) = visit ( n, graph, & mut marks, & mut stack) {
600
return Some ( c) ;
601
}
602
}
603
None
604
}
605
606
fn block < 'a > ( ctx : & mut Ctx, node : & 'a KdlNode) -> & 'a [ KdlNode] {
607
match node.children ( ) {
608
Some ( d) => d.nodes ( ) ,
609
None => {
610
ctx.err ( node.span ( ) , format! ( " `{} ` needs a block {{ … }} " , node.name ( ) .value ( ) ) ) ;
611
& [ ]
612
}
613
}
614
}
615
616
fn parse_sandbox ( ctx : & mut Ctx, node : & KdlNode) -> Option< Sandbox> {
617
let name = ctx.one_string ( node) ? ;
618
let named = ctx.name_ok ( node.span ( ) , " sandbox" , & name) ;
619
let ( mut runtime, mut rootfs, mut memory, mut cpus, mut network) =
620
( Some ( Runtime:: Microvm) , None , Some ( 1024 u32 ) , Some ( 1 u32 ) , Some ( Network:: Isolated) ) ;
621
let mut seen = BTreeSet:: new( ) ;
a72abeee Andy McCall 1 month ago ↶
622
let mut env = BTreeMap:: new( ) ;
e576e086 Andy McCall 1 month ago ↶
623
let mut runs_on = Vec :: new( ) ;
02b127d2 Andy McCall 1 month ago ↶
624
let mut timeout_s = None ;
e0918b88 Andy McCall 1 month ago
625
for c in block ( ctx, node) {
626
let key = c.name ( ) .value ( ) ;
e576e086 Andy McCall 1 month ago ↶
627
if key ! = " runs-on" & & ! seen.insert ( key.to_string ( ) ) {
e0918b88 Andy McCall 1 month ago
628
ctx.err ( c.span ( ) , format! ( " `{key} ` is given twice" ) ) ;
a72abeee Andy McCall 1 month ago ↶
629
}
630
if key = = " env" {
631
parse_env ( ctx, c, & mut env) ;
632
continue ;
e0918b88 Andy McCall 1 month ago
633
}
e576e086 Andy McCall 1 month ago ↶
634
if key = = " runs-on" {
635
ctx.no_children ( c) ;
636
parse_labels ( ctx, c, & mut runs_on) ;
637
continue ;
638
}
e0918b88 Andy McCall 1 month ago
639
ctx.no_children ( c) ;
640
match key {
641
" runtime" => {
642
runtime = match ctx.one_string ( c) .as_deref ( ) {
643
Some ( " microvm" ) => Some ( Runtime:: Microvm) ,
644
Some ( " syscall" ) | Some ( " gvisor" ) => Some ( Runtime:: Syscall) ,
645
Some ( " wasm" ) => {
646
ctx.err ( c.span ( ) , " runtime \" wasm\" is not in M0 yet; use \" microvm\" or \" syscall\" " ) ;
647
None
648
}
649
Some ( other) => {
650
ctx.err ( c.span ( ) , format! ( " runtime {other:?} : expected \" microvm\" , \" syscall\" or \" wasm\" " ) ) ;
651
None
652
}
653
None => None ,
654
}
655
}
656
" rootfs" => {
657
rootfs = match ctx.one_string ( c) {
658
Some ( r) => match r.strip_prefix ( " cas://blob/" ) {
659
Some ( h) if h.len ( ) = = 64 & & h.bytes ( ) .all ( | b | b.is_ascii_hexdigit ( ) & & ! b.is_ascii_uppercase ( ) ) => {
660
Some ( h.to_string ( ) )
661
}
662
_ => {
663
ctx.err ( c.span ( ) , format! ( " rootfs {r:?} : expected \" cas://blob/\" and 64 lower-case hex digits" ) ) ;
664
None
665
}
666
} ,
667
None => None ,
668
}
669
}
670
" memory-limit" => {
671
memory = match ctx.one_string ( c) {
672
Some ( m) => match parse_memory ( & m) {
673
Some ( mib) if ( MIN_MEMORY_MIB .. = MAX_MEMORY_MIB ) .contains ( & mib) => Some ( mib as u32 ) ,
674
Some ( _ ) => {
675
ctx.err ( c.span ( ) , format! ( " memory-limit {m:?} must be from 64MiB to 16GiB" ) ) ;
676
None
677
}
678
None => {
679
ctx.err ( c.span ( ) , format! ( " memory-limit {m:?} : expected a whole number of KiB, MiB or GiB, as \" 4GiB\" " ) ) ;
680
None
681
}
682
} ,
683
None => None ,
684
}
685
}
686
" cpu-limit" => {
687
cpus = match ctx.one_integer ( c) {
688
Some ( n) if ( 1 .. = MAX_CPUS ) .contains ( & n) => Some ( n as u32 ) ,
689
Some ( n) => {
690
ctx.err ( c.span ( ) , format! ( " cpu-limit {n} must be from 1 to {MAX_CPUS} " ) ) ;
691
None
692
}
693
None => None ,
694
}
695
}
696
" network" => {
697
network = match ctx.one_string ( c) .as_deref ( ) {
698
Some ( " isolated" ) => Some ( Network:: Isolated) ,
eb5c4db6 Andy McCall 1 month ago ↶
699
700
701
Some ( " loopback" ) => Some ( Network:: Loopback) ,
702
Some ( " egress" ) => {
703
ctx.err ( c.span ( ) , " network \" egress\" is not in M0; M0 has \" isolated\" and \" loopback\" " ) ;
e0918b88 Andy McCall 1 month ago
704
None
705
}
706
Some ( other) => {
707
ctx.err ( c.span ( ) , format! ( " network {other:?} : expected \" isolated\" , \" loopback\" or \" egress\" " ) ) ;
708
None
709
}
710
None => None ,
711
}
712
}
02b127d2 Andy McCall 1 month ago ↶
713
" timeout" => timeout_s = parse_timeout ( ctx, c) ,
e0918b88 Andy McCall 1 month ago
714
other => ctx.err ( c.span ( ) , format! ( " unknown sandbox setting `{other} `" ) ) ,
715
}
716
}
717
if rootfs.is_none ( ) & & ! seen.contains ( " rootfs" ) {
718
ctx.err ( node.span ( ) , format! ( " sandbox {name:?} needs a rootfs" ) ) ;
719
}
720
if ! named {
721
return None ;
e576e086 Andy McCall 1 month ago ↶
722
}
02b127d2 Andy McCall 1 month ago ↶
723
Some ( Sandbox { name, runtime: runtime? , rootfs: rootfs? , memory_mib: memory? , cpus: cpus? , network: network? , env, runs_on, timeout_s } )
bf7bb1b8 Andy McCall 1 month ago ↶
724
}
725
726
727
728
fn parse_on ( ctx : & mut Ctx, node : & KdlNode) -> On {
729
let mut on = On:: default( ) ;
730
if ! node.entries ( ) .is_empty ( ) {
731
ctx.err ( node.span ( ) , " `on` takes a block: on { push \" main\" ; tag \" v*\" ; manual }" ) ;
732
}
733
let globs = | ctx : & mut Ctx, c : & KdlNode| -> Vec< String> {
734
let g = ctx.strings ( c) ;
735
for p in & g {
736
if let Err ( e) = globset:: Glob:: new( p) {
737
ctx.err ( c.span ( ) , format! ( " {p:?} : {e} " ) ) ;
738
}
739
}
740
g
741
} ;
742
for c in block ( ctx, node) {
743
ctx.no_children ( c) ;
744
match c.name ( ) .value ( ) {
745
" push" => on.push.get_or_insert_with ( Vec :: new) .extend ( globs ( ctx, c) ) ,
746
" tag" => on.tag.get_or_insert_with ( Vec :: new) .extend ( globs ( ctx, c) ) ,
747
" manual" => on.manual = true ,
748
" schedule" => {
749
for line in ctx.strings ( c) {
750
if crate :: cron:: Cron:: parse( & line) .is_some ( ) {
751
on.schedule.push ( line) ;
752
} else {
753
ctx.err (
754
c.span ( ) ,
755
format! ( " schedule {line:?} : expected five cron fields (minute hour day month weekday), as \" 0 3 * * *\" " ) ,
756
) ;
757
}
758
}
759
}
760
other => ctx.err ( c.span ( ) , format! ( " unknown `on` event `{other} `; expected push, tag, manual or schedule" ) ) ,
761
}
762
}
763
if on.push.is_none ( ) & & on.tag.is_none ( ) & & ! on.manual & & on.schedule.is_empty ( ) {
764
ctx.err ( node.span ( ) , " `on` names no event, so nothing would run it" ) ;
765
}
766
on
e576e086 Andy McCall 1 month ago ↶
767
}
768
02b127d2 Andy McCall 1 month ago ↶
769
770
pub const MAX_TIMEOUT_S : u64 = 24 * 3600 ;
771
772
773
774
pub fn parse_duration ( s : & str ) -> Option< u64 > {
775
let ( mut total, mut num, mut last) = ( 0 u64 , String :: new( ) , 0 u8 ) ;
776
for ch in s.chars ( ) {
777
if ch.is_ascii_digit ( ) {
778
num.push ( ch) ;
779
continue ;
780
}
781
let ( unit, mult) = match ch {
782
' h' => ( 1 , 3600 ) ,
783
' m' => ( 2 , 60 ) ,
784
' s' => ( 3 , 1 ) ,
785
_ => return None ,
786
} ;
787
788
if num.is_empty ( ) | | unit < = last {
789
return None ;
790
}
791
last = unit;
792
total = total.checked_add ( num.parse:: < u64 > ( ) .ok ( ) ? .checked_mul ( mult) ? ) ? ;
793
num.clear ( ) ;
794
}
795
( num.is_empty ( ) & & total > 0 ) .then_some ( total)
796
}
797
798
e87c1c13 Andy McCall 20 days ago ↶
799
800
801
fn parse_priority ( ctx : & mut Ctx, c : & KdlNode) -> Option< String> {
802
ctx.no_children ( c) ;
803
let p = ctx.one_string ( c) ? ;
804
match p.as_str ( ) {
805
" low" | " normal" => Some ( p) ,
806
" high" | " urgent" => {
807
ctx.err (
808
c.span ( ) ,
809
format! ( " priority {p:?} : a pipeline may lower its jobs (`low`, `normal`), not raise them; higher levels are set on the hub, and a person may expedite a run" ) ,
810
) ;
811
None
812
}
813
_ => {
814
ctx.err ( c.span ( ) , format! ( " priority {p:?} : expected `low` or `normal`" ) ) ;
815
None
816
}
817
}
818
}
819
02b127d2 Andy McCall 1 month ago ↶
820
fn parse_timeout ( ctx : & mut Ctx, c : & KdlNode) -> Option< u64 > {
821
let t = ctx.one_string ( c) ? ;
822
match parse_duration ( & t) {
823
Some ( secs) if ( 60 .. = MAX_TIMEOUT_S ) .contains ( & secs) => Some ( secs) ,
824
Some ( _ ) => {
825
ctx.err ( c.span ( ) , format! ( " timeout {t:?} must be from 1m to 24h" ) ) ;
826
None
827
}
828
None => {
829
ctx.err ( c.span ( ) , format! ( " timeout {t:?} : expected hours, minutes and seconds, as \" 90m\" or \" 1h30m\" " ) ) ;
830
None
831
}
832
}
833
}
834
e576e086 Andy McCall 1 month ago ↶
835
836
837
fn parse_labels ( ctx : & mut Ctx, c : & KdlNode, into : & mut Vec< String> ) {
838
let labels = ctx.strings ( c) ;
839
if labels.is_empty ( ) {
840
ctx.err ( c.span ( ) , " `runs-on` needs one or more labels, as runs-on \" riscv64\" " ) ;
a72abeee Andy McCall 1 month ago ↶
841
}
e576e086 Andy McCall 1 month ago ↶
842
for l in labels {
843
let ok = ! l.is_empty ( )
844
& & l.len ( ) < = 64
845
& & l.bytes ( ) .all ( | b | b.is_ascii_lowercase ( ) | | b.is_ascii_digit ( ) | | matches! ( b, b ' -' | b ' _' | b ' .' ) ) ;
846
if ok {
847
into.push ( l) ;
848
} else {
849
ctx.err ( c.span ( ) , format! ( " runs-on {l:?} : a label is lower-case letters, digits, -, _ and ., at most 64" ) ) ;
850
}
851
}
852
into.sort ( ) ;
853
into.dedup ( ) ;
a72abeee Andy McCall 1 month ago ↶
854
}
855
856
857
fn parse_env ( ctx : & mut Ctx, c : & KdlNode, env : & mut BTreeMap< String, String> ) {
858
if ! c.entries ( ) .is_empty ( ) {
859
ctx.err ( c.span ( ) , " `env` takes a block of NAME \" value\" lines" ) ;
860
}
861
for e in block ( ctx, c) {
862
let k = e.name ( ) .value ( ) .to_string ( ) ;
863
let valid =
864
! k.is_empty ( ) & & ! k.starts_with ( | c : char | c.is_ascii_digit ( ) ) & & k.bytes ( ) .all ( | b | b.is_ascii_alphanumeric ( ) | | b = = b ' _' ) ;
865
if ! valid {
866
ctx.err ( e.span ( ) , format! ( " environment name {k:?} must be letters, digits and _" ) ) ;
867
continue ;
868
}
869
ctx.no_children ( e) ;
870
if let Some ( v) = ctx.one_string ( e) {
871
if env.insert ( k.clone ( ) , v) .is_some ( ) {
872
ctx.err ( e.span ( ) , format! ( " environment {k} is set twice" ) ) ;
873
}
874
}
e0918b88 Andy McCall 1 month ago
875
}
876
}
877
fc10d9e8 Andy McCall 1 month ago ↶
878
879
fn parse_mount ( ctx : & mut Ctx, node : & KdlNode) -> Option< Mount> {
880
let ( mut source, mut target, mut size) = ( None , None , None ) ;
881
let mut seen = BTreeSet:: new( ) ;
882
for c in block ( ctx, node) {
883
let key = c.name ( ) .value ( ) ;
884
if ! seen.insert ( key.to_string ( ) ) {
885
ctx.err ( c.span ( ) , format! ( " `{key} ` is given twice" ) ) ;
886
continue ;
887
}
888
ctx.no_children ( c) ;
889
match key {
890
" source" => {
891
source = match ctx.one_string ( c) .as_deref ( ) {
892
Some ( " ephemeral://tmpfs" ) => Some ( MountSource:: Tmpfs) ,
893
Some ( " ephemeral://disk" ) => Some ( MountSource:: Disk) ,
894
Some ( other) => {
895
ctx.err (
896
c.span ( ) ,
897
format! ( " mount source {other:?} is not in M0 yet; M0 mounts \" ephemeral://tmpfs\" or \" ephemeral://disk\" " ) ,
898
) ;
899
None
900
}
901
None => None ,
902
}
903
}
904
" target" => {
905
target = match ctx.one_string ( c) {
906
Some ( t) if mount_target_ok ( & t) => Some ( t) ,
907
Some ( t) => {
908
ctx.err (
909
c.span ( ) ,
910
format! ( " mount target {t:?} must be an absolute path under /src or /tmp, without . or .. or a trailing /" ) ,
911
) ;
912
None
913
}
914
None => None ,
915
}
916
}
917
" size" => {
918
size = match ctx.one_string ( c) {
919
Some ( m) => match parse_memory ( & m) {
920
Some ( mib) if ( MIN_MOUNT_MIB .. = MAX_MOUNT_MIB ) .contains ( & mib) => Some ( mib as u32 ) ,
921
Some ( _ ) => {
922
ctx.err ( c.span ( ) , format! ( " mount size {m:?} must be from 16MiB to 64GiB" ) ) ;
923
None
924
}
925
None => {
926
ctx.err ( c.span ( ) , format! ( " mount size {m:?} : expected a whole number of KiB, MiB or GiB, as \" 8GiB\" " ) ) ;
927
None
928
}
929
} ,
930
None => None ,
931
}
932
}
933
" readonly" | " include" => ctx.err ( c.span ( ) , format! ( " mount `{key} ` is not in M0 yet" ) ) ,
934
other => ctx.err ( c.span ( ) , format! ( " unknown mount setting `{other} `" ) ) ,
935
}
936
}
937
for ( name, missing) in [ ( " source" , source.is_none ( ) ) , ( " target" , target.is_none ( ) ) , ( " size" , size.is_none ( ) ) ] {
938
if missing & & ! seen.contains ( name) {
939
ctx.err ( node.span ( ) , format! ( " a mount needs a {name} " ) ) ;
940
}
941
}
942
Some ( Mount { source: source? , target: target? , size_mib: size? } )
943
}
944
a67b97e9 Andy McCall 1 month ago ↶
945
946
fn parse_output ( ctx : & mut Ctx, node : & KdlNode, job : & mut Job) {
947
if ! node.entries ( ) .is_empty ( ) {
948
ctx.err ( node.span ( ) , " `output` takes a block of artifacts" ) ;
949
}
950
for a in block ( ctx, node) {
951
if a.name ( ) .value ( ) ! = " artifact" {
952
ctx.err ( a.span ( ) , format! ( " unknown output `{} `; outputs are `artifact \" name\" {{ path \" /src/…\" }} `" , a.name ( ) .value ( ) ) ) ;
953
continue ;
954
}
955
let Some ( name) = ctx.one_string ( a) else { continue } ;
956
if ! ctx.name_ok ( a.span ( ) , " artifact" , & name) {
957
continue ;
958
}
959
let mut path = None ;
960
for c in block ( ctx, a) {
961
ctx.no_children ( c) ;
962
match c.name ( ) .value ( ) {
963
" path" => match ctx.one_string ( c) {
964
Some ( p) if output_path_ok ( & p) => path = Some ( p) ,
965
Some ( p) => ctx.err (
966
c.span ( ) ,
967
format! ( " artifact path {p:?} must be an absolute path under /src, without . or .., quotes or spaces" ) ,
968
) ,
969
None => { }
970
} ,
971
" format" => {
972
if ctx.one_string ( c) .as_deref ( ) ! = Some ( " blob" ) {
973
ctx.err ( c.span ( ) , " artifact format is \" blob\" " ) ;
974
}
975
}
976
other => ctx.err ( c.span ( ) , format! ( " unknown artifact setting `{other} `" ) ) ,
977
}
978
}
979
let Some ( path) = path else {
980
ctx.err ( a.span ( ) , format! ( " artifact {name:?} needs a path" ) ) ;
981
continue ;
982
} ;
983
if job.outputs.iter ( ) .any ( | o | o.name = = name ) {
984
ctx.err ( a.span ( ) , format! ( " artifact {name:?} is given twice" ) ) ;
985
} else if job.outputs.len ( ) = = MAX_OUTPUTS {
986
ctx.err ( a.span ( ) , format! ( " a job has at most {MAX_OUTPUTS} artifacts" ) ) ;
987
} else {
988
job.outputs.push ( Output { name, path } ) ;
989
}
990
}
991
}
992
e0918b88 Andy McCall 1 month ago
993
fn parse_job ( ctx : & mut Ctx, node : & KdlNode) -> Option< Job> {
994
let name = ctx.one_string ( node) ? ;
995
let named = ctx.name_ok ( node.span ( ) , " job" , & name) ;
fc10d9e8 Andy McCall 1 month ago ↶
996
let mut job = Job {
997
name: name.clone ( ) ,
998
sandbox: String :: new( ) ,
999
scope: vec! [ ] ,
1000
depends_on: vec! [ ] ,
1001
env: BTreeMap:: new( ) ,
1002
exec: vec! [ ] ,
1003
mounts: vec! [ ] ,
a67b97e9 Andy McCall 1 month ago ↶
1004
outputs: vec! [ ] ,
e576e086 Andy McCall 1 month ago ↶
1005
runs_on: vec! [ ] ,
02b127d2 Andy McCall 1 month ago ↶
1006
timeout_s: None ,
bf7bb1b8 Andy McCall 1 month ago ↶
1007
on: None ,
e87c1c13 Andy McCall 20 days ago ↶
1008
priority: None ,
fc10d9e8 Andy McCall 1 month ago ↶
1009
} ;
e0918b88 Andy McCall 1 month ago
1010
let mut seen = BTreeSet:: new( ) ;
a67b97e9 Andy McCall 1 month ago ↶
1011
let mut outputs_at = None ;
e0918b88 Andy McCall 1 month ago
1012
for c in block ( ctx, node) {
1013
let key = c.name ( ) .value ( ) ;
e576e086 Andy McCall 1 month ago ↶
1014
if key ! = " scope" & & key ! = " depends-on" & & key ! = " mount" & & key ! = " runs-on" & & ! seen.insert ( key.to_string ( ) ) {
e0918b88 Andy McCall 1 month ago
1015
ctx.err ( c.span ( ) , format! ( " `{key} ` is given twice" ) ) ;
1016
}
1017
match key {
1018
" sandbox" => {
1019
ctx.no_children ( c) ;
1020
job.sandbox = ctx.one_string ( c) .unwrap_or_default ( ) ;
1021
}
1022
" scope" => {
1023
ctx.no_children ( c) ;
1024
for g in ctx.strings ( c) {
1025
match globset:: Glob:: new( & g) {
1026
Ok ( _ ) => job.scope.push ( g) ,
1027
Err ( e) => ctx.err ( c.span ( ) , format! ( " scope {g:?} : {e} " ) ) ,
1028
}
1029
}
1030
}
1031
" depends-on" => {
1032
ctx.no_children ( c) ;
1033
job.depends_on.extend ( ctx.strings ( c) ) ;
e576e086 Andy McCall 1 month ago ↶
1034
}
1035
" runs-on" => {
1036
ctx.no_children ( c) ;
1037
parse_labels ( ctx, c, & mut job.runs_on) ;
02b127d2 Andy McCall 1 month ago ↶
1038
}
1039
" timeout" => {
1040
ctx.no_children ( c) ;
1041
job.timeout_s = parse_timeout ( ctx, c) ;
e0918b88 Andy McCall 1 month ago
1042
}
e87c1c13 Andy McCall 20 days ago ↶
1043
" priority" => job.priority = parse_priority ( ctx, c) ,
bf7bb1b8 Andy McCall 1 month ago ↶
1044
" on" => job.on = Some ( parse_on ( ctx, c) ) ,
a72abeee Andy McCall 1 month ago ↶
1045
" env" => parse_env ( ctx, c, & mut job.env) ,
e0918b88 Andy McCall 1 month ago
1046
" exec" => {
1047
ctx.no_children ( c) ;
1048
job.exec = ctx.strings ( c) ;
1049
if job.exec.is_empty ( ) {
1050
ctx.err ( c.span ( ) , " `exec` needs a program and its arguments" ) ;
1051
} else if job.exec.iter ( ) .any ( | a | a.contains ( ' \0 ' ) ) {
1052
ctx.err ( c.span ( ) , " `exec` arguments cannot contain NUL" ) ;
1053
}
1054
}
fc10d9e8 Andy McCall 1 month ago ↶
1055
" mount" => {
1056
if ! c.entries ( ) .is_empty ( ) {
1057
ctx.err ( c.span ( ) , " `mount` takes a block of source, target and size" ) ;
1058
}
1059
if let Some ( m) = parse_mount ( ctx, c) {
1060
if job.mounts.len ( ) = = MAX_MOUNTS {
1061
ctx.err ( c.span ( ) , format! ( " a job has at most {MAX_MOUNTS} mounts" ) ) ;
1062
} else if let Some ( other) = job.mounts.iter ( ) .find ( | o | {
1063
o.target = = m.target
1064
| | m.target.starts_with ( & format! ( " {} /" , o.target) )
1065
| | o.target.starts_with ( & format! ( " {} /" , m.target) )
1066
} ) {
1067
ctx.err ( c.span ( ) , format! ( " mount {:?} overlaps mount {:?} " , m.target, other.target) ) ;
1068
} else {
1069
job.mounts.push ( m) ;
1070
}
1071
}
1072
}
a67b97e9 Andy McCall 1 month ago ↶
1073
" output" => {
1074
outputs_at = Some ( c.span ( ) ) ;
1075
parse_output ( ctx, c, & mut job) ;
1076
}
e0918b88 Andy McCall 1 month ago
1077
n if LATER_JOB .contains ( & n) => ctx.err ( c.span ( ) , format! ( " `{n} ` is not in M0 yet" ) ) ,
1078
other => ctx.err ( c.span ( ) , format! ( " unknown job setting `{other} `" ) ) ,
1079
}
1080
}
fc10d9e8 Andy McCall 1 month ago ↶
1081
job.mounts.sort_by ( | a , b | a.target.cmp ( & b.target) ) ;
a67b97e9 Andy McCall 1 month ago ↶
1082
job.outputs.sort_by ( | a , b | a.name.cmp ( & b.name) ) ;
1083
1084
for o in & job.outputs {
1085
if let Some ( m) = job.mounts.iter ( ) .find ( | m | m.source = = MountSource:: Tmpfs & & o.path.starts_with ( & format! ( " {} /" , m.target) ) ) {
1086
ctx.err (
1087
outputs_at.unwrap_or ( node.span ( ) ) ,
1088
format! (
1089
" artifact {:?} is on the tmpfs mount {:?} , which is gone when the job ends; use ephemeral://disk" ,
1090
o.name, m.target
1091
) ,
1092
) ;
1093
}
1094
}
e0918b88 Andy McCall 1 month ago
1095
if job.sandbox.is_empty ( ) & & ! seen.contains ( " sandbox" ) {
1096
ctx.err ( node.span ( ) , format! ( " job {name:?} needs a sandbox" ) ) ;
1097
}
1098
if job.exec.is_empty ( ) & & ! seen.contains ( " exec" ) {
1099
ctx.err ( node.span ( ) , format! ( " job {name:?} needs an exec" ) ) ;
1100
}
1101
job.depends_on.sort ( ) ;
1102
job.depends_on.dedup ( ) ;
1103
1104
1105
named.then_some ( job)
1106
}