1use std::{collections::HashMap, fs::DirEntry};
2
3use pyo3::prelude::*;
4
5use crate::{
6 builders::builder_trait::{Builder, BuilderImpl},
7 config,
8 downloaders::{Downloader, DownloaderImpl},
9 file_manager::{recursive_list_dir, PATH_SEP},
10 flavours, log, modulefile,
11 python_interop::{extract_object, load_program},
12 shell::Shell,
13};
14
15pub fn get_submodule_path(parent: &str, submodule: &str) -> String {
16 format!("{parent}/sccmod_submodules/{submodule}")
17}
18
19#[derive(Debug, Clone)]
20pub enum Dependency {
21 Class(String), Module(String), Depends(String), Deny(String), }
26
27#[derive(Debug, Clone)]
28pub enum Environment {
29 Set(String),
30 SetExact(String),
31 Append(String),
32 Prepend(String),
33}
34
35#[derive(Debug, Clone)]
36pub struct Module {
37 pub name: String,
39
40 pub version: String,
42
43 pub class: String,
45
46 pub dependencies: Vec<Dependency>,
48
49 pub metadata: HashMap<String, String>,
51
52 pub environment: Vec<(String, Environment)>,
54
55 pub pre_build: Option<Vec<String>>,
57
58 pub post_install: Option<Vec<String>>,
60
61 pub downloader: Option<Downloader>,
63
64 pub builder: Option<Builder>,
66
67 pub source_path: String,
68 pub build_path: String,
69 pub install_path: String,
70}
71
72impl Module {
73 pub fn parse(
79 &self,
80 flavour: &(&[Module], usize),
81 ) -> (String, String, String, Vec<String>) {
82 let mut flavour_str = format!("{PATH_SEP}1{PATH_SEP}"); if flavour.1 == 0 {
87 flavour_str.push_str(&format!("default"))
88 } else {
89 for (i, flav) in (0..flavour.1).zip(flavour.0.iter()) {
90 flavour_str
91 .push_str(&format!("{}-{}", &flav.name, &flav.version));
92
93 if i + 1 < flavour.1 {
94 flavour_str.push('-');
95 }
96 }
97 }
98
99 let build_path = self.build_path.clone() + &flavour_str;
100 let install_path = self.install_path.clone() + &flavour_str;
101
102 let modules: Vec<String> =
104 flavour.0.iter().map(|flav| flav.mod_name()).collect();
105
106 (flavour_str, build_path, install_path, modules)
107 }
108
109 pub fn identifier(&self) -> String {
110 format!("{}/{}/{}", self.class, self.name, self.version)
111 }
112
113 pub fn mod_name(&self) -> String {
114 format!("{}/{}", self.name, self.version)
115 }
116
117 pub fn download(&self) -> Result<(), String> {
124 if let Some(downloader) = &self.downloader {
125 downloader.download(&self.source_path)
126 } else {
127 log::warn(&format!(
128 "Module '{}' does not hav a builder",
129 self.identifier()
130 ));
131
132 Ok(())
133 }
134 }
135
136 pub fn build(
142 &self,
143 flavour: (&[Self], usize), ) -> Result<(), String> {
145 if let Some(builder) = &self.builder {
146 if let Some(commands) = &self.pre_build {
147 log::status("Running pre-build commands");
148 let mut shell = Shell::default();
149 shell.set_current_dir(&self.source_path);
150 for cmd in commands {
151 shell.add_command(cmd);
152 }
153
154 let (result, stdout, stderr) = shell.exec();
155
156 let result =
157 result.map_err(|_| "Failed to run CMake command")?;
158
159 if !result.success() {
160 return Err(format!(
161 "Failed to execute command. Output:\n{}\n{}",
162 stdout.join("\n"),
163 stderr.join("\n")
164 ));
165 }
166
167 log::status("Building...");
168 }
169
170 let (_, build_path, install_path, modules) = self.parse(&flavour);
171
172 builder.build(
173 &self.source_path,
174 &build_path,
175 &install_path,
176 &modules,
177 )
178 } else {
179 log::warn(&format!(
180 "Module '{}' does not have a Builder",
181 self.identifier()
182 ));
183 Ok(())
184 }
185 }
186
187 pub fn install(&self, flavour: (&[Module], usize)) -> Result<(), String> {
194 if let Some(builder) = &self.builder {
195 let (_, build_path, install_path, modules) = self.parse(&flavour);
196
197 builder.install(
198 &self.source_path,
199 &build_path,
200 &install_path,
201 &modules,
202 )?;
203
204 if let Some(commands) = &self.post_install {
205 log::status(&"Running post-install commands");
206 let mut shell = Shell::default();
207 shell.set_current_dir(&install_path);
208
209 for module in &modules {
210 shell.add_command(&format!("module load {}", module));
211 }
212
213 for cmd in commands {
214 shell.add_command(&cmd);
215 }
216
217 let (result, stdout, stderr) = shell.exec();
218
219 let result = result
220 .map_err(|_| "Failed to run post-install commands")?;
221
222 if !result.success() {
223 return Err(format!(
224 "Failed to execute command. Output:\n{}\n{}",
225 stdout.join("\n"),
226 stderr.join("\n")
227 ));
228 }
229
230 log::status(&"Building...");
231 }
232
233 Ok(())
234 } else {
235 log::warn(&format!(
236 "Module '{}' does not have a Builder",
237 self.identifier()
238 ));
239 Ok(())
240 }
241 }
242
243 pub fn from_object(
249 object: &Bound<PyAny>,
250 config: &config::Config,
251 ) -> Result<Self, String> {
252 Python::with_gil(|_| {
253 let metadata: HashMap<String, String> =
254 extract_object(object, "metadata")?
255 .call0()
256 .map_err(|err| format!("Failed to call `metadata`: {err}"))?
257 .extract()
258 .map_err(|err| {
259 format!(
260 "Failed to convert metadata output to Rust HashMap: {err}"
261 )
262 })?;
263
264 let name = metadata
265 .get("name")
266 .ok_or("metadata does not contain key 'name'")?
267 .to_owned();
268
269 let version = metadata
270 .get("version")
271 .ok_or("Metadata does not contain key 'version'")?
272 .to_owned();
273
274 let class = metadata
275 .get("class")
276 .ok_or("Metadata does not contain key 'class'")?
277 .to_owned();
278
279 let downloader: Result<Option<Downloader>, String> =
280 match object.getattr("download") {
281 Ok(download) => Ok(Some(Downloader::from_py(
282 &download.call0().map_err(|err| {
283 format!(
284 "Failed to call `download` in module class: {err}"
285 )
286 })?,
287 )?)),
288 Err(_) => Ok(None),
289 };
290 let downloader = downloader?;
291
292 let dependencies: Vec<&PyAny> = extract_object(
293 object,
294 "dependencies",
295 )?
296 .call0()
297 .map_err(|err| {
298 format!("Failed to call `build_requirements`: {err}")
299 })?
300 .extract()
301 .map_err(|err| {
302 format!("Failed to convert `dependencies()` to Rust Vec: {err}")
303 })?;
304
305 let mut dependencies: Vec<Dependency> = dependencies.iter().map(|dep| {
307 match dep.get_type().to_string().as_ref() {
308 "<class 'sccmod.module.Class'>" => {
309 match dep.getattr("name").map_err(|err| format!("Dependency is a Class instance, but does not contain a .name attribute: {err}"))?.extract::<String>() {
310 Ok(name) => {
311 Ok(Dependency::Class(name))
312 },
313 Err(e) => Err(format!("Could not convert .name attribute to Rust String: {e}"))
314 }
315 },
316 "<class 'sccmod.module.Deny'>" => {
317 match dep.getattr("name").map_err(|err| format!("Dependency is a Deny instance, but does not contain a .name attribute: {err}"))?.extract::<String>() {
318 Ok(name) => {
319 Ok(Dependency::Deny(name))
320 },
321 Err(e) => Err(format!("Could not convert .name attribute to Rust String: {e}"))
322 }
323 },
324 "<class 'sccmod.module.Depends'>" => {
325 match dep.getattr("name").map_err(|err| format!("Dependency is a Depends instance, but does not contain a .name attribute: {err}"))?.extract::<String>() {
326 Ok(name) => {
327 Ok(Dependency::Depends(name))
328 },
329 Err(e) => Err(format!("Could not convert .name attribute to Rust String: {e}"))
330 }
331 },
332 _ => Ok(Dependency::Module(dep.to_string())),
333 }
334 }).collect::<Result<Vec<Dependency>, String>>()?;
335
336 let environment: Vec<(String, (String, String))> = extract_object(
337 object,
338 "environment",
339 )?
340 .call0()
341 .map_err(|err| format!("Failed to call '.environment()': {err}"))?
342 .extract()
343 .map_err(|err| {
344 format!("Failed to convert output of `.environment()` to Rust Vec<(String, (String, String))>: {err}")
345 })?;
346
347 let environment = environment
349 .into_iter()
350 .map(|(name, (op, value))| match op.as_ref() {
351 "set" => Ok((name, Environment::Set(value))),
352 "setexact" => Ok((name, Environment::SetExact(value))),
353 "append" => Ok((name, Environment::Append(value))),
354 "prepend" => Ok((name, Environment::Prepend(value))),
355 other => Err(format!(
356 "Invalid environment variable operation '{other}'"
357 )),
358 })
359 .collect::<Result<Vec<(String, Environment)>, String>>()?;
360
361 let builder: Result<Option<Builder>, String> = match object
362 .getattr("build")
363 {
364 Ok(download) => Ok(Some(Builder::from_py(
365 &download.call0().map_err(|err| {
366 format!("Failed to call `build` in module class: {err}")
367 })?,
368 )?)),
369 Err(_) => Ok(None),
370 };
371 let builder = builder?;
372
373 let pre_build: Option<Vec<String>> = match extract_object(object, "pre_build") {
374 Ok(obj) => Some(
375 obj.call0()
376 .map_err(|err| {
377 format!("Failed to call 'pre_build()` in module class: {err}")
378 })?
379 .extract()
380 .map_err(|err| {
381 format!("Failed to convert object to Rust Vec<String>: {err}")
382 })?,
383 ),
384 Err(_) => None,
385 };
386
387 let post_install: Option<Vec<String>> = match extract_object(object, "post_install") {
388 Ok(obj) => Some(
389 obj.call0()
390 .map_err(|err| {
391 format!("Failed to call 'post_install()` in module class: {err}")
392 })?
393 .extract()
394 .map_err(|err| {
395 format!("Failed to convert object to Rust Vec<String>: {err}")
396 })?,
397 ),
398 Err(_) => None,
399 };
400
401 let source_path = format!(
402 "{}{PATH_SEP}{}{PATH_SEP}{}",
403 config.build_root, name, version
404 );
405
406 let build_path = format!("{source_path}/sccmod_build");
407
408 let install_path = format!(
409 "{1:}{0:}{2:}{0:}{3:}-{4:}",
410 PATH_SEP, config.install_root, class, name, version
411 );
412
413 Ok(Self {
414 name,
415 version,
416 class,
417 dependencies,
418 environment,
419 metadata,
420 pre_build,
421 post_install,
422 downloader,
423 builder,
424 source_path,
425 build_path,
426 install_path,
427 })
428 })
429 }
430
431 pub fn modulefile(&self) -> Result<(), String> {
432 log::status(&format!("Writing Modulefile for {}", self.mod_name()));
434 let conf = config::read()?;
435 let dir = format!(
436 "{}{PATH_SEP}{}{PATH_SEP}{}{PATH_SEP}{}",
437 conf.modulefile_root, self.class, self.name, self.version
438 );
439 let dir = std::path::Path::new(&dir);
440
441 let content = modulefile::generate(&self);
442
443 std::fs::create_dir_all(dir.parent().unwrap()).unwrap();
444 std::fs::write(dir, content)
445 .map_err(|err| format!("Failed to write modulefile: {err}"))
446 }
447}
448
449pub fn get_modules() -> Result<Vec<Module>, String> {
456 config::read().and_then(|config| {
457 config .sccmod_module_paths
459 .iter()
460 .flat_map(|path| {
461 recursive_list_dir(path).map_or_else(
463 || vec![Err("Failed to extract paths".to_string())],
464 |paths| {
465 paths.into_iter().map(Ok).collect()
467 },
468 )
469 })
470 .collect::<Result<Vec<DirEntry>, _>>()? .iter()
472 .map(|path| {
473 Python::with_gil(|py| {
475 let program = load_program(&py, &path.path())?;
476 let modules: Vec<_> = program
477 .getattr("generate")
478 .map_err(|err| format!("Failed to load generator: {err}"))?
479 .call0()
480 .map_err(|err| format!("Failed to call generator: {err}"))?
481 .extract()
482 .map_err(|err| {
483 format!("Failed to convert output of `generate` to Vec: {err}")
484 })?;
485
486 modules .iter()
488 .map(|module| Module::from_object(module, &config))
489 .collect::<Result<Vec<Module>, String>>()
490 })
491 })
492 .flat_map(|v| {
493 v.map_or_else(
495 |err| vec![Err(format!("Something went wrong: {err}"))],
496 |vec| vec.into_iter().map(Ok).collect(),
497 )
498 })
499 .collect::<Result<Vec<_>, _>>() })
501}
502
503pub fn download(module: &Module) -> Result<(), String> {
508 log::status(&format!("Downloading '{}-{}'", module.name, module.version));
509 module.download()
510}
511
512pub fn build(module: &Module) -> Result<(), String> {
517 download(module)?;
518
519 log::status(&format!("Building '{}-{}'", module.name, module.version));
520
521 let flavs = flavours::generate(module)?;
522
523 for flav in &flavs {
524 log::info(&format!("Building flavour {}", flavours::gen_name(flav)));
525 module.build((&flav.0, flav.1))?;
526 }
527
528 Ok(())
529}
530
531pub fn install(module: &Module) -> Result<(), String> {
536 build(module)?;
537
538 log::status(&format!("Installing '{}-{}'", module.name, module.version));
539
540 let flavs = flavours::generate(module)?;
541
542 for flav in &flavs {
543 log::info(&format!("Installing flavour {}", flavours::gen_name(flav)));
544 module.install((&flav.0, flav.1))?;
545 }
546
547 module.modulefile()
548}
549
550pub fn modulefile(module: &Module) -> Result<(), String> {
551 module.modulefile()
552}