#[link(wasm_import_module = "sip:workflow/env")] extern "C" { #[link_name = "workflow_file_len"] fn workflow_file_len(path_ptr: *const u8, path_len: i32) -> i32; #[link_name = "read_workflow_file"] fn read_workflow_file( path_ptr: *const u8, path_len: i32, dst_ptr: *mut u8, dst_len: i32, ) -> i32; #[link_name = "declare_job"] fn declare_job( name_ptr: *const u8, name_len: i32, parent_ptr: *const u8, parent_len: i32, requires_ptr: *const u8, requires_len: i32, executor_ptr: *const u8, executor_len: i32, command_ptr: *const u8, command_len: i32, ) -> i32; } const EXECUTOR: &str = "unsafe-host-shell"; #[no_mangle] pub extern "C" fn _start() { let yaml = read_yaml(); declare_jobs(&yaml); } fn read_yaml() -> String { for path in [ ".sip/workflows/sip.yml", ".sip/workflows/sip.yaml", ".sip/workflows/ci.yml", ".sip/workflows/ci.yaml", ] { let len = unsafe { workflow_file_len(path.as_ptr(), path.len() as i32) }; if len <= 0 { continue; } let mut bytes = vec![0; len as usize]; let read = unsafe { read_workflow_file( path.as_ptr(), path.len() as i32, bytes.as_mut_ptr(), len, ) }; if read == len { return String::from_utf8(bytes).unwrap_or_default(); } } String::new() } fn declare_jobs(yaml: &str) { if yaml.lines().any(|line| line.trim() == "tasks:") { declare_task_list(yaml); } else { declare_job_map(yaml); } } fn declare_job_map(yaml: &str) { let mut in_jobs = false; let mut current_name: Option = None; let mut current_requires = Vec::new(); let mut current_commands = Vec::new(); for raw in yaml.lines() { let trimmed = raw.trim(); if trimmed.is_empty() || trimmed.starts_with('#') { continue; } if trimmed == "jobs:" { in_jobs = true; continue; } if !in_jobs { continue; } let indent = raw.len() - raw.trim_start().len(); if indent == 0 && !trimmed.starts_with("jobs:") { flush_job( &mut current_name, &mut current_requires, &mut current_commands, ); in_jobs = false; continue; } if indent == 2 && trimmed.ends_with(':') && !trimmed.starts_with('-') { flush_job( &mut current_name, &mut current_requires, &mut current_commands, ); current_name = Some(trimmed.trim_end_matches(':').trim().to_string()); continue; } if indent >= 4 && trimmed.starts_with("needs:") { let value = trimmed.trim_start_matches("needs:").trim(); current_requires = parse_needs(value); } if indent >= 6 && trimmed.starts_with("- ") { current_commands.push(trimmed.trim_start_matches("- ").trim().to_string()); } } flush_job( &mut current_name, &mut current_requires, &mut current_commands, ); } fn declare_task_list(yaml: &str) { let mut in_tasks = false; let mut previous: Option = None; for raw in yaml.lines() { let trimmed = raw.trim(); if trimmed.is_empty() || trimmed.starts_with('#') { continue; } if trimmed == "tasks:" { in_tasks = true; continue; } if !in_tasks { continue; } let indent = raw.len() - raw.trim_start().len(); if indent == 0 && !trimmed.starts_with("tasks:") { break; } let Some(task) = trimmed.strip_prefix("- ") else { continue; }; let Some((name, _command)) = task.split_once(':') else { continue; }; let name = name.trim(); let command = _command.trim(); if name.is_empty() { continue; } let requires = previous.clone().unwrap_or_default(); unsafe { declare_job( name.as_ptr(), name.len() as i32, core::ptr::null(), 0, requires.as_ptr(), requires.len() as i32, EXECUTOR.as_ptr(), EXECUTOR.len() as i32, command.as_ptr(), command.len() as i32, ); } previous = Some(name.to_string()); } } fn flush_job(name: &mut Option, requires: &mut Vec, commands: &mut Vec) { let Some(name) = name.take() else { return; }; let joined_requires = requires.join("\n"); let command = commands.join("\n"); unsafe { declare_job( name.as_ptr(), name.len() as i32, core::ptr::null(), 0, joined_requires.as_ptr(), joined_requires.len() as i32, EXECUTOR.as_ptr(), EXECUTOR.len() as i32, command.as_ptr(), command.len() as i32, ); } requires.clear(); commands.clear(); } fn parse_needs(value: &str) -> Vec { if value.starts_with('[') && value.ends_with(']') { return value .trim_start_matches('[') .trim_end_matches(']') .split(',') .map(|name| name.trim().trim_matches('"').trim_matches('\'')) .filter(|name| !name.is_empty()) .map(str::to_string) .collect(); } value .trim_matches('"') .trim_matches('\'') .split_whitespace() .filter(|name| !name.is_empty()) .map(str::to_string) .collect() }