1pub mod error;
2pub mod loader;
3use crate::MODULE_EXTENSION;
4use crate::RUNTIME_ARCH;
5use crate::scripts::run_script;
6use crate::settings::Settings;
7use error::{Error, ModuleError};
8use libloading::{Library, Symbol};
9use loader::{load_external_module_info, load_local_module_info, load_script};
10use log::debug;
11use log::info;
12use phlow_sdk::prelude::Value;
13use phlow_sdk::structs::{ApplicationData, ModuleData, ModuleSetup};
14use phlow_sdk::valu3::json;
15use reqwest::Client;
16use std::io::Write;
17use std::sync::{Mutex, OnceLock};
18use std::{
19 fs::File,
20 path::{Path, PathBuf},
21};
22
23pub async fn load_script_value(
24 script_absolute_path: &str,
25 print_yaml: bool,
26 print_output: crate::settings::PrintOutput,
27 analyzer: Option<&crate::analyzer::Analyzer>,
28) -> Result<(Value, String), Error> {
29 let script_loaded = load_script(script_absolute_path, print_yaml, print_output, analyzer).await?;
30 Ok((script_loaded.script, script_loaded.script_file_path))
31}
32
33enum ModuleType {
34 Binary,
35 Script,
36}
37
38struct ModuleTarget {
39 pub path: String,
40 pub module_type: ModuleType,
41}
42
43static LOADED_LIBRARIES: OnceLock<Mutex<Vec<Library>>> = OnceLock::new();
44
45fn retain_library(lib: Library) {
46 let libraries = LOADED_LIBRARIES.get_or_init(|| Mutex::new(Vec::new()));
47 let mut libraries = libraries.lock().unwrap_or_else(|err| err.into_inner());
48 libraries.push(lib);
49}
50
51#[derive(Debug, Clone)]
52pub struct Loader {
53 pub main: i32,
54 pub modules: Vec<ModuleData>,
55 pub steps: Value,
56 pub app_data: ApplicationData,
57 pub tests: Option<Value>,
58}
59
60impl Loader {
61 pub async fn load(
62 script_absolute_path: &str,
63 print_yaml: bool,
64 print_output: crate::settings::PrintOutput,
65 analyzer: Option<&crate::analyzer::Analyzer>,
66 ) -> Result<Self, Error> {
67 let script_loaded =
68 load_script(script_absolute_path, print_yaml, print_output, analyzer).await?;
69
70 let base_path = Path::new(&script_loaded.script_file_path)
71 .parent()
72 .map(|p| p.to_string_lossy().to_string())
73 .unwrap_or_else(|| "./".to_string());
74
75 Self::from_script_value(script_loaded.script, &base_path)
76 }
77
78 pub fn from_value(script: &Value, base_path: Option<&Path>) -> Result<Self, Error> {
79 let base_path = base_path
80 .map(|path| path.to_path_buf())
81 .unwrap_or_else(|| std::env::current_dir().unwrap_or_else(|_| PathBuf::from("./")));
82 let base_path = base_path.to_string_lossy().to_string();
83
84 Self::from_script_value(script.clone(), &base_path)
85 }
86
87 fn from_script_value(script: Value, base_path: &str) -> Result<Self, Error> {
88 let (main, modules) = match script.get("modules") {
89 Some(modules) => {
90 if !modules.is_array() {
91 return Err(Error::ModuleLoaderError("Modules not an array".to_string()));
92 }
93
94 let main_name = match script.get("main") {
95 Some(main) => Some(main.to_string()),
96 None => None,
97 };
98
99 let mut main = -1;
100
101 let mut modules_vec = Vec::new();
102 let modules_array = modules.as_array().unwrap();
103
104 for module in modules_array {
105 let mut module = ModuleData::try_from(module.clone())
106 .map_err(|_| Error::ModuleLoaderError("Module not found".to_string()))?;
107
108 if Some(module.name.clone()) == main_name {
109 main = modules_vec.len() as i32;
110 }
111
112 if let Some(local_path) = module.local_path {
113 let local_path_fix = format!("{}/{}", base_path, &local_path);
114 module.local_path = Some(local_path_fix);
115 }
116
117 modules_vec.push(module);
118 }
119
120 (main, modules_vec)
121 }
122 None => (-1, Vec::new()),
123 };
124
125 let steps = match script.get("steps") {
126 Some(steps) => steps.clone(),
127 None => return Err(Error::StepsNotDefined),
128 };
129
130 let name = script.get("name").map(|v| v.to_string());
131 let version = script.get("version").map(|v| v.to_string());
132 let environment = script.get("environment").map(|v| v.to_string());
133 let author = script.get("author").map(|v| v.to_string());
134 let description = script.get("description").map(|v| v.to_string());
135 let license = script.get("license").map(|v| v.to_string());
136 let repository = script.get("repository").map(|v| v.to_string());
137 let homepage = script.get("homepage").map(|v| v.to_string());
138
139 let app_data = ApplicationData {
140 name,
141 version,
142 environment,
143 author,
144 description,
145 license,
146 repository,
147 homepage,
148 };
149
150 let tests = script.get("tests").cloned();
152
153 Ok(Self {
154 main,
155 modules,
156 steps,
157 app_data,
158 tests,
159 })
160 }
161
162 fn find_module_path(module_relative_path: &str) -> Result<ModuleTarget, Error> {
163 let path = format!("{}/module.{}", module_relative_path, MODULE_EXTENSION);
164
165 debug!("Find {}...", path);
166
167 if Path::new(&path).exists() {
168 Ok(ModuleTarget {
169 path,
170 module_type: ModuleType::Binary,
171 })
172 } else {
173 let path = format!("{}/module.{}", module_relative_path, "phlow");
174
175 debug!("Find {}...", path);
176
177 if Path::new(&path).exists() {
178 Ok(ModuleTarget {
179 path,
180 module_type: ModuleType::Script,
181 })
182 } else {
183 let path = format!("{}.{}", module_relative_path, "phlow");
184
185 debug!("Find {}...", path);
186
187 if Path::new(&path).exists() {
188 Ok(ModuleTarget {
189 path,
190 module_type: ModuleType::Script,
191 })
192 } else {
193 debug!("Module not found: {}", module_relative_path);
194 Err(Error::ModuleNotFound(format!(
195 "Module not found at path: {}",
196 module_relative_path
197 )))
198 }
199 }
200 }
201 }
202
203 pub fn get_steps(&self) -> Value {
204 let steps = self.steps.clone();
205 json!({
206 "steps": steps
207 })
208 }
209
210 pub async fn download(&self, default_package_repository_url: &str) -> Result<(), Error> {
211 if !Path::new("phlow_packages").exists() {
212 std::fs::create_dir("phlow_packages").map_err(Error::FileCreateError)?;
213 }
214
215 info!("Downloading modules...");
216
217 let client = Client::new();
218
219 let mut downloads = Vec::new();
220
221 for module in &self.modules {
222 if module.local_path.is_some() {
224 info!(
225 "Module {} is a local path module, skipping download",
226 module.name
227 );
228 continue;
229 }
230
231 let module_so_path = format!(
232 "phlow_packages/{}/module.{}",
233 module.module, MODULE_EXTENSION
234 );
235 if Path::new(&module_so_path).exists() {
236 info!(
237 "Module {} ({}) already exists at {}, skipping download",
238 module.name, module.version, module_so_path
239 );
240 continue;
241 }
242
243 let base_url = match &module.repository {
244 Some(repo) => repo.clone(),
245 None => format!(
246 "{}/{}",
247 if regex::Regex::new(r"^(https?://|\.git|.*@.*)")
248 .unwrap()
249 .is_match(default_package_repository_url)
250 {
251 default_package_repository_url.to_string()
252 } else {
253 format!(
254 "https://raw.githubusercontent.com/{}",
255 default_package_repository_url
256 )
257 },
258 module
259 .repository_path
260 .clone()
261 .ok_or_else(|| Error::ModuleNotFound(module.name.clone()))?
262 ),
263 };
264
265 info!("Base URL: {}", base_url);
266
267 let version = if module.version == "latest" {
268 let metadata_url = format!("{}/metadata.json", base_url);
269 info!("Metadata URL: {}", metadata_url);
270
271 let res = client
272 .get(&metadata_url)
273 .send()
274 .await
275 .map_err(Error::GetFileError)?;
276 let metadata = {
277 let content = res.text().await.map_err(Error::BufferError)?;
278 Value::json_to_value(&content).map_err(Error::LoaderErrorJsonValu3)?
279 };
280
281 match metadata.get("latest") {
282 Some(version) => version.to_string(),
283 None => {
284 return Err(Error::VersionNotFound(ModuleError {
285 module: module.name.clone(),
286 }));
287 }
288 }
289 } else {
290 module.version.clone()
291 };
292
293 let handler = Self::download_and_extract_tarball(
294 base_url.clone(),
295 module.module.clone(),
296 version.clone(),
297 );
298
299 downloads.push(handler);
300 }
301
302 let results = futures::future::join_all(downloads).await;
303 for result in results {
304 if let Err(err) = result {
305 return Err(err);
306 }
307 }
308
309 info!("All modules downloaded and extracted successfully");
310 Ok(())
311 }
312
313 async fn download_and_extract_tarball(
314 base_url: String,
315 module: String,
316 version: String,
317 ) -> Result<(), Error> {
318 use flate2::read::GzDecoder;
319 use tar::Archive;
320
321 let tarball_name = format!("{}-{}-{}.tar.gz", module, version, RUNTIME_ARCH);
322 let target_url = format!("{}/{}", base_url.trim_end_matches('/'), tarball_name);
323 let target_path = format!("phlow_packages/{}/{}", module, tarball_name);
324
325 if Path::new(&format!(
326 "phlow_packages/{}/module.{}",
327 module, MODULE_EXTENSION
328 ))
329 .exists()
330 {
331 return Ok(());
332 }
333
334 info!(
335 "Downloading module tarball {} from {}",
336 tarball_name, target_url
337 );
338
339 if let Some(parent) = Path::new(&target_path).parent() {
340 std::fs::create_dir_all(parent).map_err(Error::FileCreateError)?;
341 }
342
343 let client = Client::new();
344 let response = client
345 .get(&target_url)
346 .send()
347 .await
348 .map_err(Error::GetFileError)?;
349 let content = response.bytes().await.map_err(Error::BufferError)?;
350
351 let mut file = File::create(&target_path).map_err(Error::FileCreateError)?;
353 file.write_all(&content).map_err(Error::CopyError)?;
354
355 let tar_gz = File::open(&target_path).map_err(Error::FileCreateError)?;
357 let decompressor = GzDecoder::new(tar_gz);
358 let mut archive = Archive::new(decompressor);
359 archive
360 .unpack(format!("phlow_packages/{}", module))
361 .map_err(Error::CopyError)?;
362
363 std::fs::remove_file(&target_path).map_err(Error::FileCreateError)?;
365
366 info!("Module extracted to phlow_packages/{}", module);
367
368 Ok(())
369 }
370
371 pub fn update_info(&mut self) {
372 debug!("update_info");
373
374 for module in &mut self.modules {
375 let value = if let Some(local_path) = &module.local_path {
376 load_local_module_info(local_path)
377 } else {
378 load_external_module_info(&module.module)
379 };
380
381 debug!("module info loaded");
382 module.set_info(value);
383 }
384 }
385}
386
387pub fn load_module(
388 setup: ModuleSetup,
389 module_name: &str,
390 module_version: &str,
391 local_path: Option<String>,
392 settings: Settings,
393) -> Result<(), Error> {
394 let target = {
395 let module_relative_path = match local_path {
396 Some(local_path) => local_path,
397 None => format!("phlow_packages/{}", module_name),
398 };
399
400 let target = Loader::find_module_path(&module_relative_path)?;
401
402 info!(
403 "🧪 Load Module: {} ({}), in {}",
404 module_name, module_version, target.path
405 );
406
407 target
408 };
409
410 match target.module_type {
411 ModuleType::Script => {
412 run_script(&target.path, setup, &settings);
413 }
414 ModuleType::Binary => unsafe {
415 info!("Loading binary module: {}", target.path);
416
417 let lib: Library = match Library::new(&target.path) {
418 Ok(lib) => lib,
419 Err(err) => return Err(Error::LibLoadingError(err)),
420 };
421
422 {
423 let func: Symbol<unsafe extern "C" fn(ModuleSetup)> = match lib.get(b"plugin") {
424 Ok(func) => func,
425 Err(err) => return Err(Error::LibLoadingError(err)),
426 };
427
428 func(setup);
429 }
430
431 retain_library(lib);
432 },
433 }
434
435 Ok(())
436}