char/avril
(wip) why'd you have to go and make things so complicaaaaated?
git clone https://git.t4t.associates/char/avril
a29d09f
main
1use std:: collections:: BTreeMap ; 2use std:: path::{ Path , PathBuf }; 3use std:: process:: ExitCode ; 4use std:: sync:: atomic::{ AtomicBool , Ordering }; 5use std:: sync::{ Arc , mpsc}; 6use std:: time::{ Duration , Instant }; 7 8use avril_runtime:: config::{ Error as ConfigError , ErrorKind , Span }; 9use avril_runtime:: package_config::{ PackageConfig , Task }; 10use avril_subprocess:: Scope ; 11use avril_task::{ 12Relevance , WatchContext , WatchEvent , WatchSet , WatchState , external_roots, forget_directories, 13 known_directories, remember_directories, resolution_paths, resolved_input_paths, shell_quote, 14}; 15 16use crate :: cli:: ParseError ; 17 18type Result < T > = std:: result:: Result < T , Box < dyn std:: error:: Error >>; 19type TaskResult < T > = std:: result:: Result < T , TaskError >; 20 21# [ derive ( Debug , thiserror :: Error )] 22enum TaskError { 23# [ error ( "{0}" )] 24Message ( String ), 25# [ error ( transparent )] 26Io ( # [ from ] std:: io:: Error ), 27} 28 29impl From < String > for TaskError { 30fn from ( message : String ) ->Self { 31Self :: Message ( message) 32} 33} 34 35struct Loaded { 36config : PackageConfig , 37graph : avril_task:: Graph , 38source : String , 39path : PathBuf , 40root : PathBuf , 41} 42 43fn load ( cwd : & Path ) ->Result < Loaded > { 44let path = cwd 45. ancestors () 46. map ( |directory| directory. join ( "avril.conf" )) 47. find ( |candidate| candidate. is_file ()) 48. ok_or_else ( ||{ 49format! ( 50"No avril.conf found in {} or any parent directory" , 51 cwd. display () 52) 53}) ?; 54load_file ( path) 55} 56 57fn load_file ( path : PathBuf ) ->Result < Loaded > { 58let source = std:: fs:: read_to_string ( & path) ?; 59let base = url:: Url :: from_file_path ( & path). map_err ( |_|"Config paths must be absolute" ) ?; 60let config =PackageConfig :: parse ( & source, & base) 61. map_err ( |error| error. render ( & path. to_string_lossy (), & source)) ?; 62let root = path. parent (). unwrap (). to_path_buf (); 63let graph =to_graph ( & config. tasks ); 64Ok ( Loaded { 65 config, 66 graph, 67 source, 68 path, 69 root, 70}) 71} 72 73fn list ( tasks : & BTreeMap < String , Task >) ->String { 74if tasks. is_empty () { 75return "No tasks are defined in avril.conf" . into (); 76} 77let mut text =String :: from ( "Available tasks:" ); 78for ( name, task) in tasks{ 79 text. push_str ( & format! ( "\n- {name}" )); 80if let Some ( description) =& task. description { 81 text. push_str ( & format! ( "\n {description}" )); 82} else if let Some ( command) =& task. command { 83 text. push_str ( & format! ( "\n {}" , command. display ())); 84} else { 85let dependencies: Vec < _ > = task 86. dependencies 87. iter () 88. map ( |dependency| dependency. name . as_str ()) 89. collect (); 90 text. push_str ( & format! ( "\n dependencies: {}" , dependencies. join ( ", " ))); 91} 92} 93 text 94} 95 96pub struct Args { 97watch : bool , 98name : Option < String >, 99extra : Vec < String >, 100} 101 102/// `avril task` argument parsing failed. 103# [ derive ( Debug , thiserror :: Error )] 104pub enum TaskParseError { 105# [ error ( transparent )] 106Generic ( # [ from ] ParseError ), 107} 108 109fn task_usage () ->TaskParseError { 110ParseError :: Usage ( 111crate :: cli:: command ( "task" ) 112. map ( |command| command. usage ()) 113. unwrap_or_else ( crate :: cli:: usage), 114) 115. into () 116} 117 118pub fn parse ( args : & [ String ]) -> std:: result:: Result < Args , TaskParseError > { 119let ( watch, rest) =match args. first (). map ( String :: as_str) { 120Some ( "--watch" ) =>( true , & args[ 1 ..]), 121 _ =>( false , args), 122}; 123let Some ( name) = rest. first () else { 124if watch{ 125return Err ( task_usage ()); 126} 127return Ok ( Args { 128 watch, 129name : None , 130extra : Vec :: new (), 131}); 132}; 133if name. starts_with ( '-' ) { 134return Err ( task_usage ()); 135} 136Ok ( Args { 137 watch, 138name : Some ( name. clone ()), 139extra : rest[ 1 ..]. to_vec (), 140}) 141} 142 143pub fn run ( args : Args ) ->Result < ExitCode > { 144let cwd = std:: env:: current_dir () ?. canonicalize () ?; 145let Args { watch, name, extra} = args; 146let Some ( name) = nameelse { 147let loaded =load ( & cwd) ?; 148println! ( "{}" , list ( & loaded. config . tasks )); 149return Ok ( ExitCode :: SUCCESS ); 150}; 151let loaded =load ( & cwd) ?; 152if !loaded. config . tasks . contains_key ( & name) { 153return Err ( format! ( "Unknown task {name:?}\n\n{}" , list ( & loaded. config . tasks )). into ()); 154} 155let mut extra = extra. as_slice (); 156if extra. first (). is_some_and ( |arg| arg =="--" ) { 157 extra =& extra[ 1 ..]; 158} 159let host = avril_host:: Host :: new () ?; 160let handle = host. handle (); 161let _enter = handle. enter (); 162if watch{ 163return watch_run ( & cwd, loaded, & name, extra); 164} 165match execute_graph ( & loaded, & cwd, & name, extra, None , false ) { 166Ok ( code) =>Ok ( ExitCode :: from ( code)), 167Err ( error) =>Err ( error. into ()), 168} 169} 170 171const DEBOUNCE : Duration =Duration :: from_millis ( 50 ); 172 173static SHUTDOWN : AtomicBool =AtomicBool :: new ( false ); 174 175# [ cfg ( unix )] 176fn install_shutdown_handler () ->Result <()> { 177extern "C" fn handler ( _signal : libc:: c_int ) { 178SHUTDOWN . store ( true , Ordering :: SeqCst ); 179} 180for signalin [ libc:: SIGINT , libc:: SIGTERM ] { 181if unsafe { libc:: signal ( signal, handleras * const () as libc:: sighandler_t ) } 182 == libc:: SIG_ERR 183{ 184return Err ( std:: io:: Error :: last_os_error (). into ()); 185} 186} 187Ok (()) 188} 189 190# [ cfg ( not ( unix ))] 191fn install_shutdown_handler () ->Result <()> { 192 ctrlc:: set_handler ( ||SHUTDOWN . store ( true , Ordering :: SeqCst )) ?; 193Ok (()) 194} 195 196fn create_watcher ( 197loaded : & Loaded , 198watch_set : & WatchSet , 199extra : & [ PathBuf ], 200sender : mpsc:: Sender < notify:: Result < notify:: Event >>, 201) ->Result < notify:: RecommendedWatcher > { 202use notify:: Watcher ; 203let config = notify:: Config :: default (). with_follow_symlinks ( false ); 204let mut watcher = notify:: RecommendedWatcher :: new ( 205move |result|{ 206let _ = sender. send ( result); 207}, 208 config, 209) ?; 210 watcher. watch ( & loaded. root , notify:: RecursiveMode :: Recursive ) ?; 211for parentin external_roots ( & loaded. root , watch_set, extra) { 212 watcher. watch ( & parent, notify:: RecursiveMode :: NonRecursive ) ?; 213} 214Ok ( watcher) 215} 216 217/// One watched run: its thread (while still tracked) and the control 218/// owning its process groups. A generation whose thread cannot be joined 219/// yet is retired with its handle rather than joined unconditionally. 220struct Generation { 221handle : Option < std:: thread:: JoinHandle < TaskResult < u8 >>>, 222control : Arc < Scope >, 223forced : bool , 224} 225 226impl Generation { 227/// Escalates INT, TERM, KILL against a generation's groups. Returns whether 228/// everything settled: the run thread (if still tracked) finished and no 229/// group has live members left. Settling is bounded even after KILL, so a 230/// member stuck in uninterruptible sleep cannot hang the watcher. 231fn stop ( & mut self , name : & str ) ->bool { 232if !self . control . terminate ( ||{ 233self . handle 234. as_ref () 235. is_none_or ( |handle| handle. is_finished ()) 236}) { 237eprintln! ( 238"Task {name}: some processes did not exit after SIGKILL; leaving them behind" 239); 240return false ; 241} 242if let Some ( handle) =self . handle . take () { 243let _ = handle. join (); 244} 245true 246} 247} 248 249fn watch_run ( cwd : & Path , loaded : Loaded , name : & str , extra : & [ String ]) ->Result < ExitCode > { 250if cfg! ( not ( any ( 251 target_os ="linux" , 252 target_os ="android" , 253 target_os ="macos" , 254 windows 255))) { 256return Err ( "Task --watch is not supported on this platform" . into ()); 257} 258let watch_set = loaded 259. graph 260. watch_set ( name, & loaded. root ) 261. map_err ( |error|render_task_error ( & loaded, error)) ?; 262if watch_set. patterns . is_empty () { 263return Err ( format! ( 264"Task {name:?} cannot be watched: neither it nor its dependencies declare 'files'" 265) 266. into ()); 267} 268install_shutdown_handler () ?; 269let mut loaded =Arc :: new ( loaded); 270let mut watch_set = watch_set; 271let ( sender, receiver) = mpsc:: channel (); 272let mut config_paths =resolution_paths ( & loaded. path ); 273let mut input_paths =resolved_input_paths ( & loaded. root , & watch_set); 274let linked: Vec < PathBuf > = config_paths. iter (). chain ( & input_paths). cloned (). collect (); 275let mut _watcher =create_watcher ( & loaded, & watch_set, & linked, sender. clone ()) ?; 276eprintln! ( "Task {name}: watching for changes" ); 277let mut baseline =WatchState :: capture ( 278& loaded. graph , 279& loaded. root , 280& loaded. source , 281 name, 282 extra, 283& watch_set, 284); 285 286macro_rules ! start_run{ 287( $force: expr) =>{{ 288let control =Arc :: new ( Scope :: new ( 289& std:: env:: current_exe () ?, 290vec! [ 291( "SIGINT" . into (), Duration :: from_secs ( 1 )), 292( "SIGTERM" . into (), Duration :: from_secs ( 1 )), 293], 294) ?); 295let run_runtime = tokio:: runtime:: Handle :: current (); 296let run_loaded = loaded. clone (); 297let run_cwd = cwd. to_path_buf (); 298let run_name = name. to_owned (); 299let run_extra = extra. to_vec (); 300let run_control = control. clone (); 301let force = $force; 302let handle = std:: thread:: spawn ( move ||{ 303let _enter = run_runtime. enter (); 304execute_graph ( 305& run_loaded, 306& run_cwd, 307& run_name, 308& run_extra, 309Some ( run_control), 310 force, 311) 312}); 313Some ( Generation { 314handle : Some ( handle), 315 control, 316forced : force, 317}) 318}}; 319} 320 321let mut active =start_run! ( false ); 322// Generations whose thread finished but whose process groups may 323// still hold background processes; cleaned up before the next run 324// and at shutdown, not silently dropped. 325let mut retired: Vec < Generation > =Vec :: new (); 326// A forced (cache-bypassing) refresh was launched but not yet seen to 327// complete uncancelled; the obligation carries over to the next run. 328let mut force_pending =false ; 329let mut pending =false ; 330let mut rewatch_needed =false ; 331// When the next watcher-repair retry is due, if one is scheduled. 332// Set only by failed installations, consumed when it fires, and 333// cleared by any successful one: it cannot bypass a fresh event's 334// debounce or spin against a permanently broken config. 335let mut rewatch_retry_at: Option < Instant > =None ; 336let mut installed_external =external_roots ( & loaded. root , & watch_set, & linked); 337let mut known_dirs =known_directories ( & loaded. root ); 338let mut last_event =Instant :: now (); 339 340// Coverage repair (config symlink target, external topology) is 341// independent of whether the config parses and of content debounce: 342// it only (re)installs watches by path. 343macro_rules ! repair_coverage{ 344() =>{{ 345// One installation, against the candidate config paths: a 346// successful rebuild with stale paths must not be able to 347// erase a failed candidate repair's obligation. 348let candidate_config =resolution_paths ( & loaded. path ); 349if candidate_config != config_paths || rewatch_needed{ 350let linked: Vec < PathBuf > = candidate_config 351. iter () 352. chain ( & input_paths) 353. cloned () 354. collect (); 355match create_watcher ( & loaded, & watch_set, & linked, sender. clone ()) { 356Ok ( fresh_watcher) =>{ 357 _watcher = fresh_watcher; 358 config_paths = candidate_config; 359 installed_external =external_roots ( & loaded. root , & watch_set, & linked); 360 rewatch_needed =false ; 361 rewatch_retry_at =None ; 362} 363Err ( error) =>{ 364 rewatch_retry_at =Some ( Instant :: now () +Duration :: from_secs ( 1 )); 365eprintln! ( "Task {name}: cannot rewatch: {error}" ) 366} 367} 368} 369}}; 370} 371 372loop { 373if SHUTDOWN . load ( Ordering :: SeqCst ) { 374if let Some ( mut generation) = active. take () 375 && !generation. stop ( name) 376{ 377 retired. push ( generation); 378} 379 retired. retain_mut ( |generation| !generation. stop ( name)); 380for generationin & retired{ 381 generation. control . force_close (); 382} 383return Ok ( ExitCode :: SUCCESS ); 384} 385 386// Drain in bounded batches so an event flood cannot starve the 387// shutdown check above. 388for _in 0 ..1024 { 389match receiver. try_recv () { 390Ok ( Ok ( event)) =>{ 391remember_directories ( & loaded. root , & mut known_dirs, & event. paths ); 392let relevance =if event. need_rescan () { 393// Content may have been missed and directory watches may be 394// gone with it: rebuild the watcher, not just the state. 395Some ( Relevance { evaluate : true , rewatch : true }) 396} else if matches! ( event. kind , notify:: EventKind :: Access ( _)) { 397None 398} else { 399 watch_set. classify ( 400WatchContext { 401root : & loaded. root , 402config_path : & loaded. path , 403config_paths : & config_paths, 404input_paths : & input_paths, 405known_dirs : & known_dirs, 406}, 407WatchEvent { 408paths : & event. paths , 409structural : matches! ( 410 event. kind , 411 notify:: EventKind :: Create ( _) 412 | notify:: EventKind :: Remove ( _) 413 | notify:: EventKind :: Modify ( 414 notify:: event:: ModifyKind :: Name ( _) 415) 416 | notify:: EventKind :: Any 417 | notify:: EventKind :: Other 418), 419has_directory : event. paths . iter (). any ( |path| path. is_dir ()), 420is_folder_event : matches! ( 421 event. kind , 422 notify:: EventKind :: Create ( notify:: event:: CreateKind :: Folder ) 423 | notify:: EventKind :: Remove ( 424 notify:: event:: RemoveKind :: Folder 425) 426), 427}, 428) 429}; 430if let Some ( relevance) = relevance{ 431 pending |= relevance. evaluate ; 432 rewatch_needed |= relevance. rewatch ; 433 last_event =Instant :: now (); 434} 435let gone: & [ PathBuf ] =match & event. kind { 436 notify:: EventKind :: Remove ( _) =>& event. paths , 437 notify:: EventKind :: Modify ( notify:: event:: ModifyKind :: Name ( 438 notify:: event:: RenameMode :: From , 439)) =>& event. paths , 440 notify:: EventKind :: Modify ( notify:: event:: ModifyKind :: Name ( 441 notify:: event:: RenameMode :: Both , 442)) =>& event. paths [ ..1 . min ( event. paths . len ())], 443 _ =>& [], 444}; 445forget_directories ( & loaded. root , & mut known_dirs, gone); 446} 447Ok ( Err ( error)) =>{ 448eprintln! ( "Task {name}: filesystem watcher error: {error}" ); 449 pending =true ; 450 rewatch_needed =true ; 451 last_event =Instant :: now (); 452} 453Err ( mpsc:: TryRecvError :: Empty ) =>break , 454Err ( mpsc:: TryRecvError :: Disconnected ) =>{ 455return Err ( "Filesystem watcher stopped" . into ()); 456} 457} 458} 459 460if active. as_ref (). is_some_and ( |generation|{ 461 generation 462. handle 463. as_ref () 464. is_some_and ( |handle| handle. is_finished ()) 465}) { 466let mut generation = active. take (). unwrap (); 467let forced = generation. forced ; 468let handle = generation. handle . take (). unwrap (); 469match handle. join () { 470Ok ( Ok ( 0 )) =>{ 471if forced{ 472 force_pending =false ; 473} 474} 475Ok ( Ok ( code)) =>{ 476eprintln! ( "Task {name} exited with status {code}; watching for changes" ) 477} 478Ok ( Err ( error)) =>eprintln! ( "{error}" ), 479Err ( _) =>eprintln! ( "Task {name}: task thread panicked" ), 480} 481 retired. push ( generation); 482} 483 484let retry_due = rewatch_retry_at. is_some_and ( |deadline|Instant :: now () >= deadline); 485let debouncing = pending && last_event. elapsed () <DEBOUNCE ; 486if retry_due{ 487 rewatch_retry_at =None ; 488} 489if retry_due && debouncing{ 490// A due repair must not let unfinished edits skip their quiet 491// period: repair coverage now, evaluate content later. 492if rewatch_needed{ 493repair_coverage! (); 494} 495} else if ( pending && !debouncing) || retry_due{ 496 pending =false ; 497repair_coverage! (); 498// Evaluate against a freshly loaded config before touching a 499// running task: an unchanged state (a touch, a same-content 500// save) leaves it and the cache exactly as they were. 501let mut committed =true ; 502match load_file ( loaded. path . clone ()) { 503Ok ( fresh) =>match fresh 504. graph 505. watch_set ( name, & fresh. root ) 506. map_err ( |error|render_task_error ( & fresh, error)) 507{ 508Ok ( fresh_set) if !fresh_set. patterns . is_empty () =>{ 509if fresh. source != loaded. source || fresh_set != watch_set{ 510// Commit nothing until the new set is really 511// being watched. 512let fresh_config =resolution_paths ( & fresh. path ); 513let fresh_inputs =resolved_input_paths ( & fresh. root , & fresh_set); 514let fresh_extra: Vec < PathBuf > = 515 fresh_config. iter (). chain ( & fresh_inputs). cloned (). collect (); 516match create_watcher ( & fresh, & fresh_set, & fresh_extra, sender. clone ()) { 517Ok ( fresh_watcher) =>{ 518 loaded =Arc :: new ( fresh); 519 watch_set = fresh_set; 520 config_paths = fresh_config; 521 input_paths = fresh_inputs; 522 _watcher = fresh_watcher; 523 installed_external = 524external_roots ( & loaded. root , & watch_set, & fresh_extra); 525 rewatch_retry_at =None ; 526} 527Err ( error) =>{ 528 committed =false ; 529 rewatch_retry_at = 530Some ( Instant :: now () +Duration :: from_secs ( 1 )); 531eprintln! ( "Task {name}: cannot rewatch: {error}" ) 532} 533} 534} 535} 536Ok ( _) =>{ 537 committed =false ; 538eprintln! ( 539"Task {name:?} cannot be watched: neither it nor its dependencies declare 'files'" 540); 541} 542Err ( error) =>{ 543 committed =false ; 544eprintln! ( "{error}" ) 545} 546}, 547Err ( error) =>{ 548 committed =false ; 549eprintln! ( "Task {name}: cannot reload avril.conf: {error}" ) 550} 551} 552 553if committed{ 554 known_dirs =known_directories ( & loaded. root ); 555 config_paths =resolution_paths ( & loaded. path ); 556 input_paths =resolved_input_paths ( & loaded. root , & watch_set); 557let linked: Vec < PathBuf > = 558 config_paths. iter (). chain ( & input_paths). cloned (). collect (); 559// Directory topology outside the package (an env file's 560// parent appearing or being replaced) changes coverage 561// without changing the config; reconcile it here. 562let desired_external =external_roots ( & loaded. root , & watch_set, & linked); 563if rewatch_needed || desired_external != installed_external{ 564match create_watcher ( & loaded, & watch_set, & linked, sender. clone ()) { 565Ok ( fresh_watcher) =>{ 566 _watcher = fresh_watcher; 567 installed_external = desired_external; 568 rewatch_needed =false ; 569 rewatch_retry_at =None ; 570} 571Err ( error) =>{ 572 rewatch_needed =true ; 573 rewatch_retry_at =Some ( Instant :: now () +Duration :: from_secs ( 1 )); 574eprintln! ( "Task {name}: cannot rewatch: {error}" ) 575} 576} 577} 578 579let state =WatchState :: capture ( 580& loaded. graph , 581& loaded. root , 582& loaded. source , 583 name, 584 extra, 585& watch_set, 586); 587if state != baseline{ 588let force = force_pending || state. metadata_changed ( & baseline); 589 baseline = state; 590if let Some ( mut generation) = active. take () { 591eprintln! ( "Task {name}: restarting" ); 592if !generation. stop ( name) { 593 retired. push ( generation); 594} 595} 596 retired. retain_mut ( |generation| !generation. stop ( name)); 597if SHUTDOWN . load ( Ordering :: SeqCst ) { 598continue ; 599} 600if force{ 601 force_pending =true ; 602} 603 active =start_run! ( force); 604} 605} 606} 607 608 std:: thread:: sleep ( Duration :: from_millis ( 5 )); 609} 610} 611 612// ------------------------------------------------------------- lib driver 613 614struct CliReporter ; 615 616impl avril_task:: Reporter for CliReporter { 617fn cached ( & self , name : & str ) { 618eprintln! ( "Task {name} (cached)" ); 619} 620fn unmatched_output ( & self , name : & str , pattern : & str ) { 621eprintln! ( "Task {name}: output pattern {pattern:?} matched nothing" ); 622} 623fn cache_error ( & self , name : & str , message : & str ) { 624eprintln! ( "Task {name}: cannot record cached outputs: {message}" ); 625} 626} 627 628fn to_graph ( tasks : & BTreeMap < String , Task >) -> avril_task:: Graph { 629 avril_task:: Graph { 630tasks : tasks 631. iter () 632. map ( |( name, task) |{ 633( 634 name. clone (), 635 avril_task:: Task { 636description : task. description . clone (), 637action : match & task. command { 638None => avril_task:: Action :: None , 639Some ( avril_runtime:: package_config:: TaskCommand :: Shell ( command)) =>{ 640 avril_task:: Action :: Shell ( command. clone ()) 641} 642Some ( avril_runtime:: package_config:: TaskCommand :: Argv ( argv)) =>{ 643 avril_task:: Action :: Argv ( argv. clone ()) 644} 645}, 646dependencies : task 647. dependencies 648. iter () 649. map ( |dependency| dependency. name . clone ()) 650. collect (), 651parallel : task. parallel , 652env_file : task. env_file . as_ref (). map ( |file|PathBuf :: from ( & file. path )), 653files : task. files . as_ref (). map ( |patterns|{ 654 patterns 655. iter () 656. map ( |pattern| pattern. pattern . clone ()) 657. collect () 658}), 659output : task 660. output 661. iter () 662. map ( |pattern| pattern. pattern . clone ()) 663. collect (), 664env : task. env . clone (), 665}, 666) 667}) 668. collect (), 669} 670} 671 672/// Renders a lib/task error against the config, using the span of the 673/// task, dependency, or pattern it concerns when one is known. 674fn render_task_error ( loaded : & Loaded , error : avril_task:: Error ) ->String { 675let message = error. to_string (); 676let span =match & error{ 677 avril_task:: Error :: UnknownTask ( name) => loaded 678. config 679. tasks 680. values () 681. flat_map ( |task|& task. dependencies ) 682. find ( |dependency|& dependency. name == name) 683. map ( |dependency| dependency. span . clone ()) 684. or_else ( || loaded. config . tasks . get ( name). map ( |task| task. span . clone ())), 685 avril_task:: Error :: Cycle ( cycle) =>{ 686let names: Vec < _ > = cycle. split ( " -> " ). collect (); 687 names 688. iter () 689. rev () 690. nth ( 1 ) 691. and_then ( |name| loaded. config . tasks . get ( * name)) 692. and_then ( |task|{ 693 task. dependencies 694. iter () 695. find ( |dependency|Some ( dependency. name . as_str ()) == names. last (). copied ()) 696}) 697. map ( |dependency| dependency. span . clone ()) 698} 699 avril_task:: Error :: Env ( avril_task:: EnvError :: Load { path, ..}) => loaded 700. config 701. tasks 702. values () 703. filter_map ( |task| task. env_file . as_ref ()) 704. find ( |file| loaded. root . join ( & file. path ) ==* path) 705. map ( |file| file. span . clone ()), 706 avril_task:: Error :: UnmatchedFiles { task, pattern} 707 | avril_task:: Error :: Pattern { task, pattern, ..} => loaded 708. config 709. tasks 710. get ( task) 711. and_then ( |task| task. files . as_ref ()) 712. and_then ( |patterns|{ 713 patterns 714. iter () 715. find ( |candidate|& candidate. pattern == pattern) 716. map ( |candidate| candidate. span . clone ()) 717}), 718 _ =>None , 719}; 720match span{ 721Some ( span) =>render_error ( loaded, span, message), 722None => message, 723} 724} 725 726fn execute_graph ( 727loaded : & Loaded , 728cwd : & Path , 729name : & str , 730extra : & [ String ], 731control : Option < Arc < Scope >>, 732force : bool , 733) ->TaskResult < u8 > { 734let cancelled = avril_task:: CancelToken :: new (); 735let mut options = avril_task:: RunOptions :: new ( loaded. root . clone ()); 736 options. cache =Some ( avril_task:: Cache :: new ( 737crate :: cli:: default_cache ( cwd). join ( "task-cache" ), 738)); 739 options. force = force; 740 options. extra_args = extra. to_vec (); 741 options. reporter =Box :: new ( CliReporter ); 742 options. cancelled = cancelled. clone (); 743let runtime = tokio:: runtime:: Handle :: current (); 744let mut execution = avril_task:: Execution :: new ( loaded. graph . clone (), name, options) 745. map_err ( |error|render_task_error ( loaded, error)) ?; 746loop { 747if control 748. as_ref () 749. is_some_and ( |control| control. is_cancelled ()) 750{ 751 cancelled. cancel (); 752} 753match execution 754. step () 755. map_err ( |error|render_task_error ( loaded, error)) ? 756{ 757 avril_task:: Step :: Done ( report) =>return Ok ( report. code ), 758 avril_task:: Step :: Ready ( batch) =>{ 759if batch. is_empty () { 760return Err ( TaskError :: Message ( "Task execution stalled" . into ())); 761} 762let results = std:: thread:: scope ( |scope|{ 763let handles: Vec < _ > = batch 764. iter () 765. map ( |ready|{ 766let control = control. clone (); 767let runtime = runtime. clone (); 768 scope. spawn ( move ||run_ready ( loaded, ready, cwd, runtime, control)) 769}) 770. collect (); 771 handles 772. into_iter () 773. map ( |handle| handle. join (). map_err ( |_|"Task thread panicked" . to_owned ())) 774. collect ::< Vec < _ >>() 775}); 776for ( ready, result) in batch. iter (). zip ( results) { 777let code = result??; 778 execution. finish ( & ready. name , code); 779} 780} 781} 782} 783} 784 785fn run_ready ( 786loaded : & Loaded , 787ready : & avril_task:: ReadyTask , 788cwd : & Path , 789runtime : tokio:: runtime:: Handle , 790control : Option < Arc < Scope >>, 791) ->TaskResult < u8 > { 792let name =& ready. name ; 793let directory = loaded. root . as_path (); 794let mut options =match & ready. action { 795 avril_task:: Action :: Shell ( command) =>{ 796let mut full = command. clone (); 797for argin & ready. args { 798 full. push ( ' ' ); 799 full. push_str ( & shell_quote ( arg)); 800} 801let ( shell, flag) =if cfg! ( windows) { 802( "cmd" , "/C" ) 803} else { 804( "sh" , "-c" ) 805}; 806let mut options = avril_subprocess:: Options :: new ( shell, vec! [ flag. into (), full]); 807# [ cfg ( unix )] 808{ 809 options. command = shell. into (); 810} 811 options 812} 813 avril_task:: Action :: Argv ( argv) =>{ 814let Some (( program, args)) = argv. split_first () else { 815return Err ( format! ( "Task {name:?}: command array cannot be empty" ). into ()); 816}; 817let mut args: Vec < String > = args. to_vec (); 818 args. extend ( ready. args . iter (). cloned ()); 819 avril_subprocess:: Options :: new ( program, args) 820} 821 avril_task:: Action :: Callback ( _) =>{ 822return Err ( 823format! ( "Task {name:?}: function tasks require the avril:task module" ). into (), 824); 825} 826 avril_task:: Action :: None =>return Ok ( 0 ), 827}; 828eprintln! ( "Task {name} {}" , ready. display ()); 829 options. cwd =Some ( directory. to_owned ()); 830 options. termination . scope ="child" . into (); 831// Like `--env-file`, file values are defaults: the inherited process 832// environment wins when it already defines a name. 833 options. env = ready 834. env 835. iter () 836. filter ( |( key, _) | std:: env:: var_os ( key). is_none ()) 837. map ( |( key, value) |( key. clone (), value. clone ())) 838. collect (); 839let mut inherited: BTreeMap < _ , _ > = std:: env:: vars_os (). collect (); 840if !inherited. contains_key ( std:: ffi:: OsStr :: new ( "INIT_CWD" )) 841 && !ready. env . contains_key ( "INIT_CWD" ) 842{ 843 inherited. insert ( "INIT_CWD" . into (), cwd. as_os_str (). to_owned ()); 844} 845# [ cfg ( unix )] 846if matches! ( ready. action , avril_task:: Action :: Shell ( _)) 847 && !inherited. contains_key ( std:: ffi:: OsStr :: new ( "PATH" )) 848 && !ready. env . contains_key ( "PATH" ) 849{ 850 options. command ="/bin/sh" . into (); 851} 852if SHUTDOWN . load ( Ordering :: SeqCst ) 853 &&let Some ( control) =& control 854{ 855 control. cancel (); 856} 857let process = avril_subprocess:: Launcher { 858runtime : runtime. clone (), 859caps : avril_caps:: Capabilities :: new ( avril_caps:: Initial :: new ( false ), directory. to_owned ()), 860owners : Vec :: new (), 861} 862. spawn ( options, inherited, control. as_deref (), |_, _, _|{ 863unreachable! ( "task stdio is inherited" ) 864}) 865. map_err ( |error|format! ( "Task {name:?}: {error}" )) ?; 866let status = runtime 867. block_on ( process. wait ( true , None )) 868. map_err ( |error|format! ( "Task {name:?}: {error}" )) ? 869. ok_or_else ( ||format! ( "Task {name:?}: Task exited without a status" )) ?; 870Ok ( status. code . unwrap_or ( 1 ) as u8 ) 871} 872 873fn render_error ( loaded : & Loaded , span : Span , message : String ) ->String { 874ConfigError { 875kind : ErrorKind :: Syntax , 876 message, 877 span, 878previous : None , 879} 880. render ( & loaded. path . to_string_lossy (), & loaded. source ) 881}