runtara_workflows/
agents_library.rs

1// Copyright (C) 2025 SyncMyOrders Sp. z o.o.
2// SPDX-License-Identifier: AGPL-3.0-or-later
3//! Native library management
4//!
5//! This module handles loading the pre-compiled runtara-workflow-stdlib
6//! that workflows link against.
7
8use std::io;
9use std::path::PathBuf;
10use std::sync::OnceLock;
11use tracing::info;
12
13/// Information about the compiled native library
14#[derive(Debug, Clone)]
15pub struct NativeLibraryInfo {
16    /// Path to the runtara_workflow_stdlib .rlib file (unified library)
17    pub scenario_lib_path: PathBuf,
18    /// Path to the directory containing dependency .rlib files
19    pub deps_dir: PathBuf,
20}
21
22/// Global cache for the compiled native library info
23static NATIVE_LIBRARY: OnceLock<NativeLibraryInfo> = OnceLock::new();
24
25/// Get the stdlib crate name from environment or default.
26///
27/// Products extending runtara can set `RUNTARA_STDLIB_NAME` to use their own
28/// workflow stdlib crate that re-exports runtara-workflow-stdlib with additional agents.
29///
30/// # Default
31/// `runtara_workflow_stdlib`
32///
33/// # Example
34/// ```bash
35/// export RUNTARA_STDLIB_NAME=smo_workflow_stdlib
36/// ```
37pub fn get_stdlib_name() -> String {
38    std::env::var("RUNTARA_STDLIB_NAME").unwrap_or_else(|_| "runtara_workflow_stdlib".to_string())
39}
40
41/// Get the pre-compiled native library directory
42fn get_native_library_dir() -> PathBuf {
43    // First check if explicitly set via environment variable
44    if let Ok(cache_dir) = std::env::var("RUNTARA_NATIVE_LIBRARY_DIR") {
45        let path = PathBuf::from(cache_dir);
46        if path.exists() {
47            return path;
48        }
49    }
50
51    // Try installed location (for deb packages)
52    let installed_path = PathBuf::from("/usr/share/runtara/library_cache/native");
53    if installed_path.exists() {
54        return installed_path;
55    }
56
57    // For release builds, use the deduplicated copy in target/native_cache
58    let deduplicated_path = PathBuf::from("target/native_cache");
59    if deduplicated_path.exists() {
60        return deduplicated_path;
61    }
62
63    // For development builds, search in target/debug/build or target/release/build
64    // The build.rs outputs to OUT_DIR/native_cache/native
65    for profile in &["debug", "release"] {
66        let build_dir = PathBuf::from(format!("target/{}/build", profile));
67        if build_dir.exists() {
68            // Find the runtara-* directory
69            if let Ok(entries) = std::fs::read_dir(&build_dir) {
70                for entry in entries.flatten() {
71                    let name = entry.file_name();
72                    let name_str = name.to_string_lossy();
73                    if name_str.starts_with("runtara-") {
74                        let native_path = entry.path().join("out/native_cache/native");
75                        if native_path.exists() {
76                            return native_path;
77                        }
78                    }
79                }
80            }
81        }
82    }
83
84    // For development, check DATA_DIR
85    let data_dir = std::env::var("DATA_DIR").unwrap_or_else(|_| ".data".to_string());
86    let data_path = PathBuf::from(data_dir).join("library_cache").join("native");
87    if data_path.exists() {
88        return data_path;
89    }
90
91    // Final fallback
92    PathBuf::from(".data/library_cache/native")
93}
94
95/// Load the pre-compiled native library
96fn load_native_library() -> io::Result<NativeLibraryInfo> {
97    let lib_dir = get_native_library_dir();
98
99    if !lib_dir.exists() {
100        return Err(io::Error::other(format!(
101            "Pre-compiled native library not found. Expected at: {:?}",
102            lib_dir
103        )));
104    }
105
106    // Find the unified workflow stdlib library .rlib file
107    let stdlib_name = get_stdlib_name();
108    let scenario_lib_path = lib_dir.join(format!("lib{}.rlib", stdlib_name));
109
110    if !scenario_lib_path.exists() {
111        return Err(io::Error::other(format!(
112            "{} library not found at: {:?}",
113            stdlib_name, scenario_lib_path
114        )));
115    }
116
117    // Native deps directory
118    let deps_dir = lib_dir.join("deps");
119
120    if !deps_dir.exists() {
121        return Err(io::Error::other(format!(
122            "Native deps directory not found at: {:?}",
123            deps_dir
124        )));
125    }
126
127    tracing::debug!(
128        scenario_lib = %scenario_lib_path.display(),
129        deps_dir = %deps_dir.display(),
130        "Loaded pre-compiled native library"
131    );
132
133    Ok(NativeLibraryInfo {
134        scenario_lib_path,
135        deps_dir,
136    })
137}
138
139/// Get the compiled native library information
140///
141/// This loads the pre-compiled library that was built during `cargo build`.
142/// Subsequent calls return the cached information.
143///
144/// # Returns
145/// Library information including path to .rlib file and dependencies directory
146pub fn get_native_library() -> io::Result<NativeLibraryInfo> {
147    // Check if already initialized
148    if let Some(info) = NATIVE_LIBRARY.get() {
149        return Ok(info.clone());
150    }
151
152    // Load the pre-compiled library
153    let info = load_native_library()?;
154
155    // Cache it
156    let _ = NATIVE_LIBRARY.set(info.clone());
157
158    info!(
159        scenario_lib = %info.scenario_lib_path.display(),
160        deps_dir = %info.deps_dir.display(),
161        "Native library ready (pre-compiled during build)"
162    );
163
164    Ok(info)
165}
166
167#[cfg(test)]
168mod tests {
169    use super::*;
170    use std::env;
171    use std::sync::Mutex;
172
173    // Mutex to serialize tests that modify environment variables
174    static ENV_MUTEX: Mutex<()> = Mutex::new(());
175
176    /// Helper to set env vars for a test and restore them after
177    struct EnvGuard {
178        vars: Vec<(String, Option<String>)>,
179    }
180
181    impl EnvGuard {
182        fn new() -> Self {
183            Self { vars: Vec::new() }
184        }
185
186        fn set(&mut self, key: &str, value: &str) {
187            let old = env::var(key).ok();
188            self.vars.push((key.to_string(), old));
189            // SAFETY: Tests are serialized via ENV_MUTEX, so no concurrent access
190            unsafe { env::set_var(key, value) };
191        }
192
193        fn remove(&mut self, key: &str) {
194            let old = env::var(key).ok();
195            self.vars.push((key.to_string(), old));
196            // SAFETY: Tests are serialized via ENV_MUTEX, so no concurrent access
197            unsafe { env::remove_var(key) };
198        }
199    }
200
201    impl Drop for EnvGuard {
202        fn drop(&mut self) {
203            for (key, value) in self.vars.drain(..).rev() {
204                // SAFETY: Tests are serialized via ENV_MUTEX, so no concurrent access
205                unsafe {
206                    match value {
207                        Some(v) => env::set_var(&key, v),
208                        None => env::remove_var(&key),
209                    }
210                }
211            }
212        }
213    }
214
215    // ==========================================================================
216    // NativeLibraryInfo struct tests
217    // ==========================================================================
218
219    #[test]
220    fn test_native_library_info_debug() {
221        let info = NativeLibraryInfo {
222            scenario_lib_path: PathBuf::from("/usr/lib/libruntara_workflow_stdlib.rlib"),
223            deps_dir: PathBuf::from("/usr/lib/deps"),
224        };
225
226        let debug_str = format!("{:?}", info);
227        assert!(debug_str.contains("NativeLibraryInfo"));
228        assert!(debug_str.contains("scenario_lib_path"));
229        assert!(debug_str.contains("deps_dir"));
230    }
231
232    #[test]
233    fn test_native_library_info_clone() {
234        let info = NativeLibraryInfo {
235            scenario_lib_path: PathBuf::from("/path/to/lib.rlib"),
236            deps_dir: PathBuf::from("/path/to/deps"),
237        };
238
239        let cloned = info.clone();
240
241        assert_eq!(info.scenario_lib_path, cloned.scenario_lib_path);
242        assert_eq!(info.deps_dir, cloned.deps_dir);
243    }
244
245    #[test]
246    fn test_native_library_info_paths() {
247        let info = NativeLibraryInfo {
248            scenario_lib_path: PathBuf::from("/custom/path/libworkflow.rlib"),
249            deps_dir: PathBuf::from("/custom/path/deps"),
250        };
251
252        assert_eq!(
253            info.scenario_lib_path,
254            PathBuf::from("/custom/path/libworkflow.rlib")
255        );
256        assert_eq!(info.deps_dir, PathBuf::from("/custom/path/deps"));
257    }
258
259    // ==========================================================================
260    // get_stdlib_name tests
261    // ==========================================================================
262
263    #[test]
264    fn test_get_stdlib_name_default() {
265        let _lock = ENV_MUTEX.lock().unwrap();
266        let mut guard = EnvGuard::new();
267
268        guard.remove("RUNTARA_STDLIB_NAME");
269
270        let name = get_stdlib_name();
271        assert_eq!(name, "runtara_workflow_stdlib");
272    }
273
274    #[test]
275    fn test_get_stdlib_name_custom() {
276        let _lock = ENV_MUTEX.lock().unwrap();
277        let mut guard = EnvGuard::new();
278
279        guard.set("RUNTARA_STDLIB_NAME", "smo_workflow_stdlib");
280
281        let name = get_stdlib_name();
282        assert_eq!(name, "smo_workflow_stdlib");
283    }
284
285    #[test]
286    fn test_get_stdlib_name_custom_with_underscores() {
287        let _lock = ENV_MUTEX.lock().unwrap();
288        let mut guard = EnvGuard::new();
289
290        guard.set("RUNTARA_STDLIB_NAME", "my_custom_stdlib_name");
291
292        let name = get_stdlib_name();
293        assert_eq!(name, "my_custom_stdlib_name");
294    }
295
296    #[test]
297    fn test_get_stdlib_name_empty_uses_default() {
298        let _lock = ENV_MUTEX.lock().unwrap();
299        let mut guard = EnvGuard::new();
300
301        // Empty string is a valid value, not missing
302        guard.set("RUNTARA_STDLIB_NAME", "");
303
304        let name = get_stdlib_name();
305        // Empty string is returned since it's set
306        assert_eq!(name, "");
307    }
308
309    // ==========================================================================
310    // get_native_library_dir tests (checking env var handling)
311    // ==========================================================================
312
313    #[test]
314    fn test_get_native_library_dir_from_env() {
315        let _lock = ENV_MUTEX.lock().unwrap();
316        let mut guard = EnvGuard::new();
317
318        // Use tempdir for a path that exists
319        let temp_dir = tempfile::TempDir::new().unwrap();
320        guard.set(
321            "RUNTARA_NATIVE_LIBRARY_DIR",
322            temp_dir.path().to_str().unwrap(),
323        );
324
325        let dir = get_native_library_dir();
326        assert_eq!(dir, temp_dir.path());
327    }
328
329    #[test]
330    fn test_get_native_library_dir_env_nonexistent_falls_through() {
331        let _lock = ENV_MUTEX.lock().unwrap();
332        let mut guard = EnvGuard::new();
333
334        // Set to non-existent path - should fall through to other checks
335        guard.set("RUNTARA_NATIVE_LIBRARY_DIR", "/nonexistent/path/12345");
336        guard.remove("DATA_DIR");
337
338        let dir = get_native_library_dir();
339        // Should fall back to some other path (not the env var value)
340        assert_ne!(dir, PathBuf::from("/nonexistent/path/12345"));
341    }
342
343    #[test]
344    fn test_get_native_library_dir_data_dir_env() {
345        let _lock = ENV_MUTEX.lock().unwrap();
346        let mut guard = EnvGuard::new();
347
348        // Create a temp dir structure
349        let temp_dir = tempfile::TempDir::new().unwrap();
350        let lib_cache = temp_dir.path().join("library_cache").join("native");
351        std::fs::create_dir_all(&lib_cache).unwrap();
352
353        guard.remove("RUNTARA_NATIVE_LIBRARY_DIR");
354        guard.set("DATA_DIR", temp_dir.path().to_str().unwrap());
355
356        let dir = get_native_library_dir();
357        // May or may not use DATA_DIR depending on other paths existing
358        // Just verify it doesn't panic
359        assert!(dir.to_str().is_some());
360    }
361
362    // ==========================================================================
363    // load_native_library error cases
364    // ==========================================================================
365
366    #[test]
367    fn test_load_native_library_missing_dir() {
368        let _lock = ENV_MUTEX.lock().unwrap();
369        let mut guard = EnvGuard::new();
370
371        // Point to a non-existent directory that also won't fall through
372        let temp_dir = tempfile::TempDir::new().unwrap();
373        let nonexistent = temp_dir.path().join("nonexistent");
374        guard.set("RUNTARA_NATIVE_LIBRARY_DIR", nonexistent.to_str().unwrap());
375
376        // Clear other paths to force our env var path
377        guard.remove("DATA_DIR");
378
379        // The function internally checks if path exists before using env var
380        // So we need a different approach - just verify error handling works
381        let result = load_native_library();
382        // Either succeeds (if system has libs) or fails with appropriate error
383        if let Err(e) = result {
384            assert!(e.to_string().contains("not found") || e.to_string().contains("library"));
385        }
386    }
387
388    #[test]
389    fn test_load_native_library_missing_rlib() {
390        let _lock = ENV_MUTEX.lock().unwrap();
391        let mut guard = EnvGuard::new();
392
393        // Create a directory but no .rlib file
394        let temp_dir = tempfile::TempDir::new().unwrap();
395        guard.set(
396            "RUNTARA_NATIVE_LIBRARY_DIR",
397            temp_dir.path().to_str().unwrap(),
398        );
399        guard.remove("RUNTARA_STDLIB_NAME");
400
401        let result = load_native_library();
402        assert!(result.is_err());
403        let err = result.unwrap_err();
404        assert!(err.to_string().contains("not found"));
405    }
406
407    #[test]
408    fn test_load_native_library_missing_deps() {
409        let _lock = ENV_MUTEX.lock().unwrap();
410        let mut guard = EnvGuard::new();
411
412        // Create directory with .rlib but no deps dir
413        let temp_dir = tempfile::TempDir::new().unwrap();
414        std::fs::write(
415            temp_dir.path().join("libruntara_workflow_stdlib.rlib"),
416            b"fake rlib",
417        )
418        .unwrap();
419
420        guard.set(
421            "RUNTARA_NATIVE_LIBRARY_DIR",
422            temp_dir.path().to_str().unwrap(),
423        );
424        guard.remove("RUNTARA_STDLIB_NAME");
425
426        let result = load_native_library();
427        assert!(result.is_err());
428        let err = result.unwrap_err();
429        assert!(err.to_string().contains("deps"));
430    }
431
432    #[test]
433    fn test_load_native_library_success() {
434        let _lock = ENV_MUTEX.lock().unwrap();
435        let mut guard = EnvGuard::new();
436
437        // Create complete directory structure
438        let temp_dir = tempfile::TempDir::new().unwrap();
439        let deps_dir = temp_dir.path().join("deps");
440        std::fs::create_dir(&deps_dir).unwrap();
441        std::fs::write(
442            temp_dir.path().join("libruntara_workflow_stdlib.rlib"),
443            b"fake rlib",
444        )
445        .unwrap();
446
447        guard.set(
448            "RUNTARA_NATIVE_LIBRARY_DIR",
449            temp_dir.path().to_str().unwrap(),
450        );
451        guard.remove("RUNTARA_STDLIB_NAME");
452
453        let result = load_native_library();
454        assert!(result.is_ok());
455
456        let info = result.unwrap();
457        assert_eq!(
458            info.scenario_lib_path,
459            temp_dir.path().join("libruntara_workflow_stdlib.rlib")
460        );
461        assert_eq!(info.deps_dir, deps_dir);
462    }
463
464    #[test]
465    fn test_load_native_library_custom_stdlib_name() {
466        let _lock = ENV_MUTEX.lock().unwrap();
467        let mut guard = EnvGuard::new();
468
469        // Create directory with custom stdlib name
470        let temp_dir = tempfile::TempDir::new().unwrap();
471        let deps_dir = temp_dir.path().join("deps");
472        std::fs::create_dir(&deps_dir).unwrap();
473        std::fs::write(temp_dir.path().join("libcustom_stdlib.rlib"), b"fake rlib").unwrap();
474
475        guard.set(
476            "RUNTARA_NATIVE_LIBRARY_DIR",
477            temp_dir.path().to_str().unwrap(),
478        );
479        guard.set("RUNTARA_STDLIB_NAME", "custom_stdlib");
480
481        let result = load_native_library();
482        assert!(result.is_ok());
483
484        let info = result.unwrap();
485        assert_eq!(
486            info.scenario_lib_path,
487            temp_dir.path().join("libcustom_stdlib.rlib")
488        );
489    }
490}