rusty_stack/installers/components/
mpi4py.rs1use crate::installers::common::{command_exists, DistroFacade};
11use std::fmt;
12use std::path::PathBuf;
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
20pub enum MpiImplementation {
21 OpenMPI,
23 MPICH,
25 IntelMPI,
27 None,
29}
30
31impl fmt::Display for MpiImplementation {
32 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
33 match self {
34 MpiImplementation::OpenMPI => write!(f, "OpenMPI"),
35 MpiImplementation::MPICH => write!(f, "MPICH"),
36 MpiImplementation::IntelMPI => write!(f, "IntelMPI"),
37 MpiImplementation::None => write!(f, "None"),
38 }
39 }
40}
41
42#[derive(Debug, Clone)]
44pub struct Mpi4PyConfig {
45 pub python_bin: String,
47 pub force_reinstall: bool,
49 pub dry_run: bool,
51 pub rocm_enabled: bool,
53}
54
55impl Default for Mpi4PyConfig {
56 fn default() -> Self {
57 Self {
58 python_bin: "python3".to_string(),
59 force_reinstall: false,
60 dry_run: false,
61 rocm_enabled: true,
62 }
63 }
64}
65
66#[derive(Debug, Clone, PartialEq, Eq)]
68pub struct ShellCommand {
69 pub program: String,
71 pub args: Vec<String>,
73 pub env: Vec<(String, String)>,
75}
76
77impl ShellCommand {
78 pub fn to_command_string(&self) -> String {
80 let env_prefix = self
81 .env
82 .iter()
83 .map(|(k, v)| format!("{k}={v}"))
84 .collect::<Vec<_>>()
85 .join(" ");
86 let cmd = if self.args.is_empty() {
87 self.program.clone()
88 } else {
89 format!("{} {}", self.program, self.args.join(" "))
90 };
91 if env_prefix.is_empty() {
92 cmd
93 } else {
94 format!("{env_prefix} {cmd}")
95 }
96 }
97}
98
99pub struct Mpi4PyInstaller {
101 config: Mpi4PyConfig,
102}
103
104impl Mpi4PyInstaller {
105 pub fn new(config: Mpi4PyConfig) -> Self {
107 Self { config }
108 }
109
110 pub fn with_defaults() -> Self {
112 Self::new(Mpi4PyConfig::default())
113 }
114
115 pub fn detect_mpi(&self) -> MpiImplementation {
123 if command_exists("mpirun") || command_exists("mpiexec") {
124 if self.is_openmpi() {
126 MpiImplementation::OpenMPI
127 } else if self.is_mpich() {
128 MpiImplementation::MPICH
129 } else {
130 MpiImplementation::OpenMPI
132 }
133 } else {
134 MpiImplementation::None
135 }
136 }
137
138 fn is_openmpi(&self) -> bool {
140 command_exists("ompi_info")
142 || std::path::Path::new("/usr/lib/x86_64-linux-gnu/openmpi").exists()
143 || std::path::Path::new("/usr/lib64/openmpi").exists()
144 }
145
146 fn is_mpich(&self) -> bool {
148 command_exists("mpichversion")
149 || std::path::Path::new("/usr/lib/x86_64-linux-gnu/mpich").exists()
150 }
151
152 pub fn detect_mpi_path(&self) -> PathBuf {
160 let candidates = [
161 "/usr/lib64/openmpi",
162 "/opt/openmpi",
163 "/usr/local/openmpi",
164 "/usr/lib/openmpi",
165 ];
166
167 for path_str in &candidates {
168 let path = PathBuf::from(path_str);
169 if path.join("bin/mpirun").exists() || path.join("bin").exists() {
170 return path;
171 }
172 }
173
174 if let Ok(output) = std::process::Command::new("which").arg("mpirun").output() {
176 if output.status.success() {
177 let mpirun = String::from_utf8_lossy(&output.stdout).trim().to_string();
178 if !mpirun.is_empty() {
179 if let Some(parent) = PathBuf::from(&mpirun).parent() {
180 if let Some(grandparent) = parent.parent() {
181 return grandparent.to_path_buf();
182 }
183 }
184 }
185 }
186 }
187
188 PathBuf::from("/usr/lib64/openmpi")
189 }
190
191 pub fn build_system_mpi_install_command(&self, distro: &DistroFacade) -> ShellCommand {
203 if distro.uses_apt() {
204 ShellCommand {
205 program: "sudo".to_string(),
206 args: vec![
207 "apt-get".to_string(),
208 "update".to_string(),
209 "&&".to_string(),
210 "sudo".to_string(),
211 "apt-get".to_string(),
212 "install".to_string(),
213 "-y".to_string(),
214 "libopenmpi-dev".to_string(),
215 "openmpi-bin".to_string(),
216 ],
217 env: vec![],
218 }
219 } else if distro.uses_dnf() {
220 ShellCommand {
221 program: "sudo".to_string(),
222 args: vec![
223 "dnf".to_string(),
224 "install".to_string(),
225 "-y".to_string(),
226 "openmpi".to_string(),
227 "openmpi-devel".to_string(),
228 ],
229 env: vec![],
230 }
231 } else if distro.uses_yum() {
232 ShellCommand {
233 program: "sudo".to_string(),
234 args: vec![
235 "yum".to_string(),
236 "install".to_string(),
237 "-y".to_string(),
238 "openmpi".to_string(),
239 "openmpi-devel".to_string(),
240 ],
241 env: vec![],
242 }
243 } else if distro.uses_pacman() {
244 ShellCommand {
245 program: "sudo".to_string(),
246 args: vec![
247 "pacman".to_string(),
248 "-S".to_string(),
249 "--noconfirm".to_string(),
250 "openmpi".to_string(),
251 ],
252 env: vec![],
253 }
254 } else if distro.uses_zypper() {
255 ShellCommand {
256 program: "sudo".to_string(),
257 args: vec![
258 "zypper".to_string(),
259 "install".to_string(),
260 "-y".to_string(),
261 "openmpi".to_string(),
262 "openmpi-devel".to_string(),
263 ],
264 env: vec![],
265 }
266 } else {
267 ShellCommand {
268 program: "sudo".to_string(),
269 args: vec![
270 "apt-get".to_string(),
271 "update".to_string(),
272 "&&".to_string(),
273 "sudo".to_string(),
274 "apt-get".to_string(),
275 "install".to_string(),
276 "-y".to_string(),
277 "libopenmpi-dev".to_string(),
278 "openmpi-bin".to_string(),
279 ],
280 env: vec![],
281 }
282 }
283 }
284
285 pub fn build_pip_install_command(&self, mpi_path: &std::path::Path) -> ShellCommand {
294 let mpicc = mpi_path.join("bin/mpicc");
295 let mpicxx = mpi_path.join("bin/mpicxx");
296
297 let env = vec![
298 ("MPICC".to_string(), mpicc.to_string_lossy().to_string()),
299 ("MPICXX".to_string(), mpicxx.to_string_lossy().to_string()),
300 (
301 "PATH".to_string(),
302 format!(
303 "{}:{}",
304 mpi_path.join("bin").to_string_lossy(),
305 std::env::var("PATH").unwrap_or_default()
306 ),
307 ),
308 (
309 "LD_LIBRARY_PATH".to_string(),
310 format!(
311 "{}:{}",
312 mpi_path.join("lib").to_string_lossy(),
313 std::env::var("LD_LIBRARY_PATH").unwrap_or_default()
314 ),
315 ),
316 ];
317
318 let mut args = vec![
319 "-m".to_string(),
320 "pip".to_string(),
321 "install".to_string(),
322 "--break-system-packages".to_string(),
323 ];
324 if self.config.force_reinstall {
325 args.push("--force-reinstall".to_string());
326 }
327 args.push("mpi4py".to_string());
328
329 ShellCommand {
330 program: self.config.python_bin.clone(),
331 args,
332 env,
333 }
334 }
335}
336
337#[cfg(test)]
342mod tests {
343 use super::*;
344 use crate::platform::detection::{DistroFamily, DistroInfo, PackageManager};
345
346 fn ubuntu_distro() -> DistroFacade {
347 DistroFacade::from_info(DistroInfo {
348 id: "ubuntu".to_string(),
349 name: "Ubuntu 24.04".to_string(),
350 version: "24.04".to_string(),
351 codename: "noble".to_string(),
352 family: DistroFamily::Debian,
353 pkg_manager: PackageManager::Apt,
354 id_like: "debian".to_string(),
355 })
356 }
357
358 fn fedora_distro() -> DistroFacade {
359 DistroFacade::from_info(DistroInfo {
360 id: "fedora".to_string(),
361 name: "Fedora 41".to_string(),
362 version: "41".to_string(),
363 codename: String::new(),
364 family: DistroFamily::Rhel,
365 pkg_manager: PackageManager::Dnf,
366 id_like: "fedora".to_string(),
367 })
368 }
369
370 fn arch_distro() -> DistroFacade {
371 DistroFacade::from_info(DistroInfo {
372 id: "arch".to_string(),
373 name: "Arch Linux".to_string(),
374 version: "rolling".to_string(),
375 codename: String::new(),
376 family: DistroFamily::Arch,
377 pkg_manager: PackageManager::Pacman,
378 id_like: "arch".to_string(),
379 })
380 }
381
382 #[test]
385 fn test_detect_mpi() {
386 let installer = Mpi4PyInstaller::with_defaults();
387 let mpi = installer.detect_mpi();
389 assert!(matches!(
391 mpi,
392 MpiImplementation::OpenMPI | MpiImplementation::MPICH | MpiImplementation::None
393 ));
394 }
395
396 #[test]
397 fn test_detect_mpi_path() {
398 let installer = Mpi4PyInstaller::with_defaults();
399 let path = installer.detect_mpi_path();
400 assert!(!path.to_string_lossy().is_empty());
402 }
403
404 #[test]
405 fn test_system_mpi_install_apt() {
406 let installer = Mpi4PyInstaller::with_defaults();
407 let distro = ubuntu_distro();
408 let cmd = installer.build_system_mpi_install_command(&distro);
409 assert!(cmd.args.contains(&"libopenmpi-dev".to_string()));
410 assert!(cmd.args.contains(&"openmpi-bin".to_string()));
411 }
412
413 #[test]
414 fn test_system_mpi_install_dnf() {
415 let installer = Mpi4PyInstaller::with_defaults();
416 let distro = fedora_distro();
417 let cmd = installer.build_system_mpi_install_command(&distro);
418 assert!(cmd.args.contains(&"openmpi".to_string()));
419 assert!(cmd.args.contains(&"openmpi-devel".to_string()));
420 }
421
422 #[test]
423 fn test_system_mpi_install_pacman() {
424 let installer = Mpi4PyInstaller::with_defaults();
425 let distro = arch_distro();
426 let cmd = installer.build_system_mpi_install_command(&distro);
427 assert!(cmd.args.contains(&"openmpi".to_string()));
428 assert!(cmd.args.contains(&"--noconfirm".to_string()));
429 }
430
431 #[test]
432 fn test_pip_install_command() {
433 let installer = Mpi4PyInstaller::with_defaults();
434 let mpi_path = PathBuf::from("/usr/lib64/openmpi");
435 let cmd = installer.build_pip_install_command(&mpi_path);
436
437 assert_eq!(cmd.program, "python3");
438 assert!(cmd.args.contains(&"mpi4py".to_string()));
439 assert!(cmd.args.contains(&"--break-system-packages".to_string()));
440
441 assert!(cmd
443 .env
444 .iter()
445 .any(|(k, v)| k == "MPICC" && v.contains("mpicc")));
446 assert!(cmd
447 .env
448 .iter()
449 .any(|(k, v)| k == "MPICXX" && v.contains("mpicxx")));
450 assert!(cmd
451 .env
452 .iter()
453 .any(|(k, v)| k == "PATH" && v.contains("openmpi/bin")));
454 }
455
456 #[test]
457 fn test_pip_install_command_force_reinstall() {
458 let installer = Mpi4PyInstaller::new(Mpi4PyConfig {
459 force_reinstall: true,
460 ..Default::default()
461 });
462 let mpi_path = PathBuf::from("/usr/lib64/openmpi");
463 let cmd = installer.build_pip_install_command(&mpi_path);
464 assert!(cmd.args.contains(&"--force-reinstall".to_string()));
465 }
466
467 #[test]
468 fn test_mpi_implementation_display() {
469 assert_eq!(format!("{}", MpiImplementation::OpenMPI), "OpenMPI");
470 assert_eq!(format!("{}", MpiImplementation::MPICH), "MPICH");
471 assert_eq!(format!("{}", MpiImplementation::None), "None");
472 }
473}