1use runtara_dsl::ExecutionGraph;
11use serde_json::Value;
12use sha2::{Digest, Sha256};
13use std::collections::HashMap;
14use std::fs;
15use std::io;
16use std::path::PathBuf;
17use std::process::Command;
18use tracing::info;
19
20use crate::codegen::ast;
21use crate::paths::get_scenario_json_path;
22
23fn parse_rustc_error(stderr: &str, target: &str) -> String {
32 if stderr.contains("error[E0463]") && stderr.contains("can't find crate") {
36 if stderr.contains("std") {
37 return format!(
38 "Compilation failed: The Rust standard library for target '{}' is not installed.\n\n\
39 To fix this, run:\n rustup target add {}",
40 target, target
41 );
42 }
43 }
44
45 if stderr.contains("could not find specification for target") {
47 return format!(
48 "Compilation failed: Target '{}' is not installed.\n\n\
49 To fix this, run:\n rustup target add {}",
50 target, target
51 );
52 }
53
54 if stderr.contains("linker") && stderr.contains("not found") {
56 if target.contains("musl") {
57 return format!(
58 "Compilation failed: The musl linker is not installed.\n\n\
59 To fix this on Ubuntu/Debian, run:\n sudo apt install musl-tools\n\n\
60 To fix this on Fedora/RHEL, run:\n sudo dnf install musl-gcc"
61 );
62 }
63 }
64
65 if stderr.contains("can't find crate for") {
67 if let Some(crate_name) = extract_pattern(stderr, "can't find crate for `", "`") {
68 if crate_name == "runtara_workflow_stdlib" {
69 return format!(
70 "Compilation failed: The workflow stdlib library is not compiled.\n\n\
71 To fix this, run:\n cargo build -p runtara-workflow-stdlib --release --target {}\n\n\
72 Or set RUNTARA_NATIVE_LIBRARY_DIR to point to a pre-compiled stdlib.",
73 target
74 );
75 }
76 return format!(
77 "Compilation failed: Cannot find crate '{}'.\n\n\
78 This may indicate the workflow stdlib is not properly compiled.\n\
79 Try rebuilding: cargo build -p runtara-workflow-stdlib --release --target {}",
80 crate_name, target
81 );
82 }
83 }
84
85 if stderr.contains("error[E0432]") && stderr.contains("unresolved import") {
87 if let Some(import) = extract_pattern(stderr, "unresolved import `", "`") {
88 return format!(
89 "Compilation failed: Unresolved import '{}'.\n\n\
90 This is likely a code generation bug. Please report this issue.",
91 import
92 );
93 }
94 }
95
96 if stderr.contains("error[E0308]") && stderr.contains("mismatched types") {
98 return format!(
99 "Compilation failed: Type mismatch in generated code.\n\n\
100 This is likely a code generation bug. Please report this issue."
101 );
102 }
103
104 if stderr.contains("error[E0382]")
106 || stderr.contains("error[E0502]")
107 || stderr.contains("error[E0499]")
108 {
109 return format!(
110 "Compilation failed: Borrow checker error in generated code.\n\n\
111 This is likely a code generation bug. Please report this issue."
112 );
113 }
114
115 if let Some(first_error) = extract_first_error(stderr) {
117 return format!(
118 "Compilation failed: {}\n\n\
119 If this error persists, please contact support.",
120 first_error
121 );
122 }
123
124 "Compilation failed. Please contact support if this issue persists.".to_string()
126}
127
128fn extract_pattern<'a>(text: &'a str, prefix: &str, suffix: &str) -> Option<&'a str> {
130 let start = text.find(prefix)? + prefix.len();
131 let rest = &text[start..];
132 let end = rest.find(suffix)?;
133 Some(&rest[..end])
134}
135
136fn extract_first_error(stderr: &str) -> Option<String> {
138 for line in stderr.lines() {
139 let line = line.trim();
140 if line.starts_with("error[E") {
141 if let Some(msg_start) = line.find("]: ") {
143 let msg = &line[msg_start + 3..];
144 return Some(msg.to_string());
145 }
146 } else if line.starts_with("error:") {
147 let msg = line.trim_start_matches("error:").trim();
148 if !msg.is_empty() {
149 return Some(msg.to_string());
150 }
151 }
152 }
153 None
154}
155
156fn get_host_target() -> &'static str {
161 #[cfg(all(target_os = "macos", target_arch = "aarch64"))]
162 {
163 "aarch64-apple-darwin"
164 }
165 #[cfg(all(target_os = "macos", target_arch = "x86_64"))]
166 {
167 "x86_64-apple-darwin"
168 }
169 #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
170 {
171 "x86_64-unknown-linux-musl"
173 }
174 #[cfg(all(target_os = "linux", target_arch = "aarch64"))]
175 {
176 "aarch64-unknown-linux-musl"
178 }
179 #[cfg(not(any(
180 all(target_os = "macos", target_arch = "aarch64"),
181 all(target_os = "macos", target_arch = "x86_64"),
182 all(target_os = "linux", target_arch = "x86_64"),
183 all(target_os = "linux", target_arch = "aarch64"),
184 )))]
185 {
186 compile_error!("Unsupported platform for scenario compilation")
187 }
188}
189
190const SIDE_EFFECT_OPERATIONS: &[(&str, &str)] = &[
192 ("utils", "random-double"),
194 ("utils", "random-array"),
195 ("utils", "get-current-unix-timestamp"),
196 ("utils", "get-current-iso-datetime"),
197 ("utils", "get-current-formatted-datetime"),
198 ("utils", "delay-in-ms"),
199 ("http", "http-request"),
201 ("sftp", "sftp-list-files"),
203 ("sftp", "sftp-download-file"),
204 ("sftp", "sftp-upload-file"),
205 ("sftp", "sftp-delete-file"),
206];
207
208pub fn workflow_has_side_effects(workflow: &Value) -> bool {
216 let steps = match workflow.get("steps") {
218 Some(Value::Object(steps)) => steps,
219 _ => return false,
220 };
221
222 for (_step_id, step) in steps {
224 if let Some(Value::String(step_type)) = step.get("stepType") {
226 if step_type != "Agent" {
227 continue;
228 }
229 }
230
231 let operator_id = step
233 .get("operatorId")
234 .and_then(|v| v.as_str())
235 .map(|s| s.to_lowercase());
236 let operation_id = step
237 .get("operationId")
238 .and_then(|v| v.as_str())
239 .map(|s| s.to_lowercase());
240
241 if let (Some(operator), Some(operation)) = (operator_id, operation_id) {
242 for (side_effect_op, side_effect_operation) in SIDE_EFFECT_OPERATIONS {
244 if operator == side_effect_op.to_lowercase()
245 && operation == side_effect_operation.to_lowercase()
246 {
247 return true;
248 }
249 }
250 }
251 }
252
253 false
254}
255
256#[derive(Debug, Clone)]
261pub struct ChildDependency {
262 pub step_id: String,
264 pub child_scenario_id: String,
266 pub child_version_requested: String,
268 pub child_version_resolved: i32,
270}
271
272#[derive(Debug, Clone)]
277pub struct ChildScenarioInput {
278 pub step_id: String,
280 pub scenario_id: String,
282 pub version_requested: String,
284 pub version_resolved: i32,
286 pub execution_graph: ExecutionGraph,
288}
289
290#[derive(Debug)]
296pub struct CompilationInput {
297 pub tenant_id: String,
299 pub scenario_id: String,
301 pub version: u32,
303 pub execution_graph: ExecutionGraph,
305 pub debug_mode: bool,
307 pub child_scenarios: Vec<ChildScenarioInput>,
309 pub connection_service_url: Option<String>,
313}
314
315#[derive(Debug)]
319pub struct NativeCompilationResult {
320 pub binary_path: PathBuf,
322 pub binary_size: usize,
324 pub binary_checksum: String,
326 pub build_dir: PathBuf,
328 pub has_side_effects: bool,
330 pub child_dependencies: Vec<ChildDependency>,
332}
333
334fn get_rustc_compile_dir(tenant_id: &str, workflow_id: &str, version: u32) -> PathBuf {
336 let data_dir = std::env::var("DATA_DIR").unwrap_or_else(|_| ".data".to_string());
337 PathBuf::from(data_dir)
338 .join(tenant_id)
339 .join("scenarios")
340 .join(workflow_id)
341 .join("native_build")
342 .join(format!("version_{}", version))
343}
344
345fn get_native_libs() -> io::Result<crate::agents_library::NativeLibraryInfo> {
347 crate::agents_library::get_native_library()
348}
349
350pub fn compile_scenario(input: CompilationInput) -> io::Result<NativeCompilationResult> {
361 let CompilationInput {
362 tenant_id,
363 scenario_id,
364 version,
365 execution_graph,
366 debug_mode,
367 child_scenarios,
368 connection_service_url,
369 } = input;
370
371 let validation_result = crate::validation::validate_workflow(&execution_graph);
373
374 for warning in &validation_result.warnings {
376 tracing::warn!(
377 tenant_id = %tenant_id,
378 scenario_id = %scenario_id,
379 version = version,
380 warning = %warning,
381 "Workflow validation warning"
382 );
383 }
384
385 if validation_result.has_errors() {
387 let error_messages: Vec<String> = validation_result
388 .errors
389 .iter()
390 .map(|e| e.to_string())
391 .collect();
392
393 let warning_note = if validation_result.has_warnings() {
394 format!(
395 "\n\nAdditionally, {} warning(s) were found.",
396 validation_result.warnings.len()
397 )
398 } else {
399 String::new()
400 };
401
402 return Err(io::Error::new(
403 io::ErrorKind::InvalidData,
404 format!(
405 "Workflow validation failed with {} error(s):\n\n{}{}",
406 validation_result.errors.len(),
407 error_messages.join("\n\n"),
408 warning_note
409 ),
410 ));
411 }
412
413 let native_libs = get_native_libs()?;
415
416 let setup_start = std::time::Instant::now();
418 let build_dir = get_rustc_compile_dir(&tenant_id, &scenario_id, version);
419 fs::create_dir_all(&build_dir)?;
420
421 let child_graphs: HashMap<String, ExecutionGraph> = child_scenarios
423 .iter()
424 .map(|c| (c.step_id.clone(), c.execution_graph.clone()))
425 .collect();
426
427 let codegen_start = std::time::Instant::now();
429 let tenant_id_for_codegen = tenant_id.clone();
430 let rust_code = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
431 ast::compile_with_children(
432 &execution_graph,
433 debug_mode,
434 child_graphs,
435 connection_service_url,
436 Some(tenant_id_for_codegen),
437 )
438 }))
439 .map_err(|panic_info| {
440 let panic_msg = if let Some(s) = panic_info.downcast_ref::<&str>() {
441 s.to_string()
442 } else if let Some(s) = panic_info.downcast_ref::<String>() {
443 s.clone()
444 } else {
445 "Unknown panic during code generation".to_string()
446 };
447
448 tracing::error!(
449 tenant_id = %tenant_id,
450 scenario_id = %scenario_id,
451 version = version,
452 error = %panic_msg,
453 "Code generation panicked"
454 );
455
456 io::Error::new(
457 io::ErrorKind::InvalidData,
458 format!("Code generation failed: {}", panic_msg),
459 )
460 })?;
461 let codegen_duration = codegen_start.elapsed();
462 tracing::debug!(
463 codegen_duration_ms = codegen_duration.as_millis() as u64,
464 "Code generation completed"
465 );
466
467 let main_rs_path = build_dir.join("main.rs");
468 fs::write(&main_rs_path, rust_code)?;
469 let setup_duration = setup_start.elapsed();
470 tracing::debug!(
471 setup_duration_ms = setup_duration.as_millis() as u64,
472 "Setup completed (dirs + codegen + write)"
473 );
474
475 let binary_output_path = build_dir.join("scenario");
477
478 let compilation_start = std::time::Instant::now();
480 info!(
481 scenario_id = %scenario_id,
482 version = version,
483 mode = "native",
484 "Starting scenario compilation"
485 );
486
487 let target = get_host_target();
489 let mut cmd = Command::new("rustc");
490 cmd.arg(format!("--target={}", target))
491 .arg("--crate-type=bin")
492 .arg("--edition=2024")
493 .arg("-C")
494 .arg("opt-level=2")
495 .arg("-C")
496 .arg("codegen-units=16");
497
498 #[cfg(target_os = "linux")]
500 {
501 cmd.arg("-C").arg("target-feature=+crt-static");
502 }
503
504 let deps_dir = &native_libs.deps_dir;
506 if deps_dir.exists() {
507 cmd.arg("-L")
508 .arg(format!("dependency={}", deps_dir.display()));
509 }
510
511 if let Some(lib_dir) = native_libs.scenario_lib_path.parent() {
513 cmd.arg("-L").arg(format!("native={}", lib_dir.display()));
514 }
515
516 #[cfg(target_os = "macos")]
518 {
519 let openssl_paths = [
521 "/opt/homebrew/opt/openssl/lib", "/usr/local/opt/openssl/lib", "/opt/homebrew/opt/openssl@3/lib",
524 "/usr/local/opt/openssl@3/lib",
525 ];
526 for path in &openssl_paths {
527 if std::path::Path::new(path).exists() {
528 cmd.arg("-L").arg(format!("native={}", path));
529 break;
530 }
531 }
532 }
533
534 let stdlib_name = crate::agents_library::get_stdlib_name();
536 cmd.arg("--extern").arg(format!(
537 "{}={}",
538 stdlib_name,
539 native_libs.scenario_lib_path.display()
540 ));
541
542 #[cfg(target_os = "macos")]
544 let dylib_ext = "dylib";
545 #[cfg(target_os = "linux")]
546 let dylib_ext = "so";
547 #[cfg(target_os = "windows")]
548 let dylib_ext = "dll";
549
550 if deps_dir.exists() {
553 if let Ok(entries) = fs::read_dir(deps_dir) {
554 for entry in entries.flatten() {
555 let path = entry.path();
556 let ext = path.extension().and_then(|s| s.to_str());
557
558 if ext != Some("rlib") && ext != Some(dylib_ext) {
560 continue;
561 }
562
563 if let Some(filename) = path.file_name().and_then(|n| n.to_str()) {
564 if let Some(crate_name_part) = filename.strip_prefix("lib") {
566 if let Some(crate_name) = crate_name_part.split('-').next() {
567 let extern_name = crate_name.replace('-', "_");
569 cmd.arg("--extern")
570 .arg(format!("{}={}", extern_name, path.display()));
571 }
572 }
573 }
574 }
575 }
576 }
577
578 cmd.arg("-o").arg(&binary_output_path);
580
581 cmd.arg(&main_rs_path);
583
584 tracing::info!(
585 tenant_id = %tenant_id,
586 scenario_id = %scenario_id,
587 version = version,
588 "Invoking rustc for native compilation"
589 );
590 let rustc_start = std::time::Instant::now();
591 let output = cmd.output().map_err(|e| {
592 io::Error::other(format!(
593 "Failed to execute rustc: {}. Make sure rustc is installed with {} target.",
594 e, target
595 ))
596 })?;
597 let rustc_duration = rustc_start.elapsed();
598 tracing::info!(
599 rustc_duration_ms = rustc_duration.as_millis() as u64,
600 "Rustc compilation completed"
601 );
602
603 let stderr = String::from_utf8_lossy(&output.stderr);
605 if !output.status.success() {
606 let stdout = String::from_utf8_lossy(&output.stdout);
607 tracing::error!(
609 stderr = %stderr,
610 stdout = %stdout,
611 "Rustc compilation failed"
612 );
613
614 let user_message = parse_rustc_error(&stderr, target);
616 return Err(io::Error::other(user_message));
617 }
618
619 if !binary_output_path.exists() {
621 return Err(io::Error::other(format!(
622 "Compilation appeared to succeed but binary was not found at {:?}",
623 binary_output_path
624 )));
625 }
626
627 let io_start = std::time::Instant::now();
629 let binary_metadata = fs::metadata(&binary_output_path).map_err(|e| {
630 io::Error::other(format!(
631 "Failed to stat binary at {:?}: {}",
632 binary_output_path, e
633 ))
634 })?;
635 let binary_size = binary_metadata.len() as usize;
636
637 let mut hasher = Sha256::new();
639 let mut file = fs::File::open(&binary_output_path)?;
640 std::io::copy(&mut file, &mut hasher)?;
641 let binary_checksum = format!("{:x}", hasher.finalize());
642
643 let io_duration = io_start.elapsed();
644 tracing::debug!(
645 io_duration_ms = io_duration.as_millis() as u64,
646 binary_size_bytes = binary_size,
647 "Checksum calculated"
648 );
649
650 let scenario_json_path = get_scenario_json_path(&tenant_id, &scenario_id, version);
652 let has_side_effects = if scenario_json_path.exists() {
653 let json_content = fs::read_to_string(&scenario_json_path)?;
654 let workflow: Value = serde_json::from_str(&json_content).map_err(|e| {
655 io::Error::new(
656 io::ErrorKind::InvalidData,
657 format!("Failed to parse scenario JSON: {}", e),
658 )
659 })?;
660 workflow_has_side_effects(&workflow)
661 } else {
662 false
663 };
664
665 let compilation_duration = compilation_start.elapsed();
666 info!(
667 tenant_id = %tenant_id,
668 scenario_id = %scenario_id,
669 version = version,
670 binary_size_bytes = binary_size,
671 compilation_duration_ms = compilation_duration.as_millis() as u64,
672 has_side_effects = has_side_effects,
673 "Scenario compiled successfully"
674 );
675
676 let child_dependencies: Vec<ChildDependency> = child_scenarios
678 .iter()
679 .map(|c| ChildDependency {
680 step_id: c.step_id.clone(),
681 child_scenario_id: c.scenario_id.clone(),
682 child_version_requested: c.version_requested.clone(),
683 child_version_resolved: c.version_resolved,
684 })
685 .collect();
686
687 Ok(NativeCompilationResult {
688 binary_path: binary_output_path,
689 binary_size,
690 binary_checksum,
691 build_dir,
692 has_side_effects,
693 child_dependencies,
694 })
695}
696
697pub fn translate_scenario(
709 tenant_id: &str,
710 scenario_id: &str,
711 version: u32,
712 execution_graph: &ExecutionGraph,
713 debug_mode: bool,
714) -> io::Result<PathBuf> {
715 let build_dir = get_rustc_compile_dir(tenant_id, scenario_id, version);
717 fs::create_dir_all(&build_dir)?;
718
719 let rust_code = ast::compile(execution_graph, debug_mode);
721
722 let main_rs_path = build_dir.join("main.rs");
724 fs::write(&main_rs_path, rust_code)?;
725
726 info!(
727 "Generated Rust code for scenario {}/{} v{} at {:?}",
728 tenant_id, scenario_id, version, main_rs_path
729 );
730
731 Ok(build_dir)
732}
733
734#[cfg(test)]
735mod tests {
736 use super::*;
737
738 #[test]
739 fn test_workflow_has_side_effects_empty() {
740 let workflow: Value = serde_json::json!({
741 "steps": {}
742 });
743 assert!(!workflow_has_side_effects(&workflow));
744 }
745
746 #[test]
747 fn test_workflow_has_side_effects_http() {
748 let workflow: Value = serde_json::json!({
749 "steps": {
750 "step1": {
751 "stepType": "Agent",
752 "operatorId": "http",
753 "operationId": "http-request"
754 }
755 }
756 });
757 assert!(workflow_has_side_effects(&workflow));
758 }
759
760 #[test]
761 fn test_workflow_has_side_effects_pure() {
762 let workflow: Value = serde_json::json!({
763 "steps": {
764 "step1": {
765 "stepType": "Agent",
766 "operatorId": "transform",
767 "operationId": "map"
768 }
769 }
770 });
771 assert!(!workflow_has_side_effects(&workflow));
772 }
773}