1use crate::installers::common::DistroFacade;
19use crate::platform::detection::DistroFamily;
20
21#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27pub enum MigraphxSupport {
28 Full,
30 SystemOnly,
32 Unavailable,
34}
35
36impl std::fmt::Display for MigraphxSupport {
37 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
38 match self {
39 MigraphxSupport::Full => write!(f, "full"),
40 MigraphxSupport::SystemOnly => write!(f, "system-only"),
41 MigraphxSupport::Unavailable => write!(f, "unavailable"),
42 }
43 }
44}
45
46#[derive(Debug, Clone, PartialEq, Eq)]
52pub struct ShellCommand {
53 pub program: String,
55 pub args: Vec<String>,
57 pub env: Vec<(String, String)>,
59}
60
61impl ShellCommand {
62 pub fn to_command_string(&self) -> String {
64 let env_prefix = self
65 .env
66 .iter()
67 .map(|(k, v)| format!("{k}={v}"))
68 .collect::<Vec<_>>()
69 .join(" ");
70 let cmd = if self.args.is_empty() {
71 self.program.clone()
72 } else {
73 format!("{} {}", self.program, self.args.join(" "))
74 };
75 if env_prefix.is_empty() {
76 cmd
77 } else {
78 format!("{env_prefix} {cmd}")
79 }
80 }
81}
82
83#[derive(Debug, Clone)]
85pub struct MigraphxConfig {
86 pub python_bin: String,
88 pub dry_run: bool,
90}
91
92impl Default for MigraphxConfig {
93 fn default() -> Self {
94 Self {
95 python_bin: "python3".to_string(),
96 dry_run: false,
97 }
98 }
99}
100
101pub struct MigraphxInstaller {
103 config: MigraphxConfig,
104}
105
106impl MigraphxInstaller {
107 pub fn new(config: MigraphxConfig) -> Self {
109 Self { config }
110 }
111
112 pub fn with_defaults() -> Self {
114 Self::new(MigraphxConfig::default())
115 }
116
117 pub fn required_packages(&self, distro: &DistroFacade) -> Vec<&str> {
123 match distro.family() {
124 DistroFamily::Arch => vec!["migraphx"],
125 _ => vec!["migraphx", "migraphx-dev"],
126 }
127 }
128
129 pub fn optional_packages(&self, distro: &DistroFacade) -> Vec<&str> {
131 match distro.family() {
132 DistroFamily::Arch => vec!["python3-migraphx", "half"],
133 _ => vec!["python3-migraphx", "half"],
134 }
135 }
136
137 pub fn build_package_install_command(
139 &self,
140 distro: &DistroFacade,
141 packages: &[&str],
142 ) -> ShellCommand {
143 let (program, mut base_args) = match distro.family() {
144 DistroFamily::Debian => (
145 "sudo".to_string(),
146 vec![
147 "apt-get".to_string(),
148 "install".to_string(),
149 "-y".to_string(),
150 ],
151 ),
152 DistroFamily::Rhel => (
153 "sudo".to_string(),
154 vec!["dnf".to_string(), "install".to_string(), "-y".to_string()],
155 ),
156 DistroFamily::Arch => (
157 "sudo".to_string(),
158 vec![
159 "pacman".to_string(),
160 "-S".to_string(),
161 "--needed".to_string(),
162 "--noconfirm".to_string(),
163 ],
164 ),
165 DistroFamily::Suse => (
166 "sudo".to_string(),
167 vec![
168 "zypper".to_string(),
169 "install".to_string(),
170 "-y".to_string(),
171 ],
172 ),
173 _ => (
174 "sudo".to_string(),
175 vec![
176 "apt-get".to_string(),
177 "install".to_string(),
178 "-y".to_string(),
179 ],
180 ),
181 };
182
183 for pkg in packages {
184 base_args.push(pkg.to_string());
185 }
186
187 ShellCommand {
188 program,
189 args: base_args,
190 env: vec![],
191 }
192 }
193
194 pub fn build_package_update_command(&self, distro: &DistroFacade) -> ShellCommand {
196 let (program, args) = match distro.family() {
197 DistroFamily::Debian => (
198 "sudo".to_string(),
199 vec![
200 "apt-get".to_string(),
201 "update".to_string(),
202 "-y".to_string(),
203 ],
204 ),
205 DistroFamily::Rhel => (
206 "sudo".to_string(),
207 vec!["dnf".to_string(), "makecache".to_string()],
208 ),
209 DistroFamily::Arch => (
210 "sudo".to_string(),
211 vec!["pacman".to_string(), "-Sy".to_string()],
212 ),
213 DistroFamily::Suse => (
214 "sudo".to_string(),
215 vec!["zypper".to_string(), "refresh".to_string()],
216 ),
217 _ => (
218 "sudo".to_string(),
219 vec![
220 "apt-get".to_string(),
221 "update".to_string(),
222 "-y".to_string(),
223 ],
224 ),
225 };
226
227 ShellCommand {
228 program,
229 args,
230 env: vec![],
231 }
232 }
233
234 pub fn build_pip_install_command(&self) -> ShellCommand {
243 ShellCommand {
244 program: self.config.python_bin.clone(),
245 args: vec![
246 "-m".to_string(),
247 "pip".to_string(),
248 "install".to_string(),
249 "--break-system-packages".to_string(),
250 "migraphx".to_string(),
251 ],
252 env: vec![],
253 }
254 }
255
256 pub fn is_available_on_distro(&self, distro: &DistroFacade) -> MigraphxSupport {
267 match distro.family() {
268 DistroFamily::Arch => MigraphxSupport::SystemOnly,
269 _ => MigraphxSupport::Full,
270 }
271 }
272
273 pub fn build_limitation_message(&self, distro: &DistroFacade) -> Option<String> {
276 match self.is_available_on_distro(distro) {
277 MigraphxSupport::Full => None,
278 MigraphxSupport::SystemOnly => Some(format!(
279 "MIGraphX on {} has limited support: only the system package is available. \
280 Python bindings (pip install migraphx) are not available on Arch-family distros. \
281 The system package ({}) has been installed. \
282 If you need Python bindings, consider using the ROCm Docker image or building from source.",
283 distro.id(),
284 self.required_packages(distro).join(", ")
285 )),
286 MigraphxSupport::Unavailable => Some(format!(
287 "MIGraphX is not available on {}. \
288 Consider using the ROCm Docker image or building from source.",
289 distro.id()
290 )),
291 }
292 }
293
294 pub fn should_skip_pip_install(&self, distro: &DistroFacade) -> bool {
300 matches!(
301 self.is_available_on_distro(distro),
302 MigraphxSupport::SystemOnly
303 )
304 }
305}
306
307pub fn migraphx_driver_status() -> Result<(), Vec<String>> {
324 let driver = std::process::Command::new("/opt/rocm/bin/migraphx-driver")
325 .arg("--version")
326 .output();
327 match driver {
328 Ok(o) if o.status.success() => Ok(()),
329 Ok(_) => Err(migraphx_missing_libs()),
330 Err(_) => Err(vec![
331 "/opt/rocm/bin/migraphx-driver not found (is the migraphx package installed?)"
332 .to_string(),
333 ]),
334 }
335}
336
337pub fn migraphx_driver_functional() -> bool {
339 migraphx_driver_status().is_ok()
340}
341
342pub fn migraphx_driver_version() -> Option<String> {
351 if !migraphx_driver_functional() {
352 return None;
353 }
354 let out = std::process::Command::new("/opt/rocm/bin/migraphx-driver")
355 .arg("--version")
356 .output()
357 .ok()?;
358 if !out.status.success() {
359 return None;
360 }
361 let combined = format!(
362 "{}\n{}",
363 String::from_utf8_lossy(&out.stdout),
364 String::from_utf8_lossy(&out.stderr),
365 );
366 extract_driver_semver(&combined)
367}
368
369fn extract_driver_semver(s: &str) -> Option<String> {
382 for token in s.split_whitespace() {
383 let cleaned = token.trim_matches(|c: char| !c.is_ascii_digit() && c != '.');
384 let mut parts = cleaned.split('.');
385 let Some(maj) = parts.next() else {
388 continue;
389 };
390 let Some(min) = parts.next() else {
391 continue;
392 };
393 if !maj.chars().all(|c| c.is_ascii_digit()) {
394 continue;
395 }
396 if !min.chars().all(|c| c.is_ascii_digit()) {
397 continue;
398 }
399 match parts.next() {
400 Some(pat) if !pat.is_empty() && pat.chars().all(|c| c.is_ascii_digit()) => {
403 return Some(format!("{maj}.{min}.{pat}"));
404 }
405 Some(_) => continue,
408 None => return Some(format!("{maj}.{min}")),
410 }
411 }
412 None
413}
414
415fn migraphx_missing_libs() -> Vec<String> {
417 match std::process::Command::new("ldd")
418 .arg("/opt/rocm/bin/migraphx-driver")
419 .output()
420 {
421 Ok(o) => String::from_utf8_lossy(&o.stdout)
422 .lines()
423 .map(|l| l.trim().to_string())
424 .filter(|l| l.contains("not found"))
425 .collect(),
426 Err(_) => Vec::new(),
427 }
428}
429
430#[cfg(test)]
435mod tests {
436 use super::*;
437 use crate::platform::detection::{DistroInfo, PackageManager};
438
439 #[test]
440 fn test_extract_driver_semver_three_and_two_part() {
441 assert_eq!(
443 extract_driver_semver("MIGraphX Version: 2.15.0.20250912-dirty"),
444 Some("2.15.0".to_string())
445 );
446 assert_eq!(
449 extract_driver_semver("MIGraphX 2.12"),
450 Some("2.12".to_string())
451 );
452 assert_eq!(
454 extract_driver_semver("build foo 1.2.3 bar"),
455 Some("1.2.3".to_string())
456 );
457 assert_eq!(extract_driver_semver("MIGraphX vX.Y.Z"), None);
459 assert_eq!(extract_driver_semver("no version here"), None);
460
461 assert_eq!(
466 extract_driver_semver("2.12.x"),
467 None,
468 "2.12.x must not become 2.12"
469 );
470 assert_eq!(
471 extract_driver_semver("2.12."),
472 None,
473 "2.12. must not become 2.12"
474 );
475 assert_eq!(
477 extract_driver_semver("driver 2.12 build"),
478 Some("2.12".to_string())
479 );
480 }
481
482 #[test]
485 fn test_required_packages_debian() {
486 let installer = MigraphxInstaller::with_defaults();
487 let distro = DistroFacade::from_info(DistroInfo {
488 id: "ubuntu".to_string(),
489 family: DistroFamily::Debian,
490 pkg_manager: PackageManager::Apt,
491 ..Default::default()
492 });
493 let pkgs = installer.required_packages(&distro);
494 assert!(pkgs.contains(&"migraphx"));
495 assert!(pkgs.contains(&"migraphx-dev"));
496 }
497
498 #[test]
499 fn test_required_packages_arch() {
500 let installer = MigraphxInstaller::with_defaults();
501 let distro = DistroFacade::from_info(DistroInfo {
502 id: "arch".to_string(),
503 family: DistroFamily::Arch,
504 pkg_manager: PackageManager::Pacman,
505 ..Default::default()
506 });
507 let pkgs = installer.required_packages(&distro);
508 assert!(pkgs.contains(&"migraphx"));
509 assert!(!pkgs.contains(&"migraphx-dev"));
510 }
511
512 #[test]
513 fn test_optional_packages() {
514 let installer = MigraphxInstaller::with_defaults();
515 let distro = DistroFacade::from_info(DistroInfo {
516 id: "ubuntu".to_string(),
517 family: DistroFamily::Debian,
518 pkg_manager: PackageManager::Apt,
519 ..Default::default()
520 });
521 let pkgs = installer.optional_packages(&distro);
522 assert!(pkgs.contains(&"python3-migraphx"));
523 assert!(pkgs.contains(&"half"));
524 }
525
526 #[test]
527 fn test_package_install_debian() {
528 let installer = MigraphxInstaller::with_defaults();
529 let distro = DistroFacade::from_info(DistroInfo {
530 id: "ubuntu".to_string(),
531 family: DistroFamily::Debian,
532 pkg_manager: PackageManager::Apt,
533 ..Default::default()
534 });
535 let cmd = installer.build_package_install_command(&distro, &["migraphx", "migraphx-dev"]);
536 assert_eq!(cmd.program, "sudo");
537 assert!(cmd.args.contains(&"apt-get".to_string()));
538 assert!(cmd.args.contains(&"install".to_string()));
539 assert!(cmd.args.contains(&"migraphx".to_string()));
540 assert!(cmd.args.contains(&"migraphx-dev".to_string()));
541 }
542
543 #[test]
544 fn test_package_install_arch() {
545 let installer = MigraphxInstaller::with_defaults();
546 let distro = DistroFacade::from_info(DistroInfo {
547 id: "arch".to_string(),
548 family: DistroFamily::Arch,
549 pkg_manager: PackageManager::Pacman,
550 ..Default::default()
551 });
552 let cmd = installer.build_package_install_command(&distro, &["migraphx"]);
553 assert!(cmd.args.contains(&"pacman".to_string()));
554 assert!(cmd.args.contains(&"-S".to_string()));
555 assert!(cmd.args.contains(&"migraphx".to_string()));
556 }
557
558 #[test]
559 fn test_package_install_rhel() {
560 let installer = MigraphxInstaller::with_defaults();
561 let distro = DistroFacade::from_info(DistroInfo {
562 id: "fedora".to_string(),
563 family: DistroFamily::Rhel,
564 pkg_manager: PackageManager::Dnf,
565 ..Default::default()
566 });
567 let cmd = installer.build_package_install_command(&distro, &["migraphx", "migraphx-dev"]);
568 assert!(cmd.args.contains(&"dnf".to_string()));
569 assert!(cmd.args.contains(&"migraphx".to_string()));
570 }
571
572 #[test]
573 fn test_pip_install_command() {
574 let installer = MigraphxInstaller::with_defaults();
575 let cmd = installer.build_pip_install_command();
576 assert_eq!(cmd.program, "python3");
577 assert!(cmd.args.contains(&"-m".to_string()));
578 assert!(cmd.args.contains(&"pip".to_string()));
579 assert!(cmd.args.contains(&"install".to_string()));
580 assert!(cmd.args.contains(&"migraphx".to_string()));
581 }
582
583 #[test]
586 fn test_availability_debian_is_full() {
587 let installer = MigraphxInstaller::with_defaults();
588 let distro = DistroFacade::from_info(DistroInfo {
589 id: "ubuntu".to_string(),
590 family: DistroFamily::Debian,
591 pkg_manager: PackageManager::Apt,
592 ..Default::default()
593 });
594 assert_eq!(
595 installer.is_available_on_distro(&distro),
596 MigraphxSupport::Full
597 );
598 }
599
600 #[test]
601 fn test_availability_arch_is_system_only() {
602 let installer = MigraphxInstaller::with_defaults();
603 let distro = DistroFacade::from_info(DistroInfo {
604 id: "arch".to_string(),
605 family: DistroFamily::Arch,
606 pkg_manager: PackageManager::Pacman,
607 ..Default::default()
608 });
609 assert_eq!(
610 installer.is_available_on_distro(&distro),
611 MigraphxSupport::SystemOnly
612 );
613 }
614
615 #[test]
616 fn test_availability_cachyos_is_system_only() {
617 let installer = MigraphxInstaller::with_defaults();
618 let distro = DistroFacade::from_info(DistroInfo {
619 id: "cachyos".to_string(),
620 family: DistroFamily::Arch,
621 pkg_manager: PackageManager::Pacman,
622 ..Default::default()
623 });
624 assert_eq!(
625 installer.is_available_on_distro(&distro),
626 MigraphxSupport::SystemOnly
627 );
628 }
629
630 #[test]
631 fn test_availability_rhel_is_full() {
632 let installer = MigraphxInstaller::with_defaults();
633 let distro = DistroFacade::from_info(DistroInfo {
634 id: "fedora".to_string(),
635 family: DistroFamily::Rhel,
636 pkg_manager: PackageManager::Dnf,
637 ..Default::default()
638 });
639 assert_eq!(
640 installer.is_available_on_distro(&distro),
641 MigraphxSupport::Full
642 );
643 }
644
645 #[test]
646 fn test_limitation_message_debian_is_none() {
647 let installer = MigraphxInstaller::with_defaults();
648 let distro = DistroFacade::from_info(DistroInfo {
649 id: "ubuntu".to_string(),
650 family: DistroFamily::Debian,
651 pkg_manager: PackageManager::Apt,
652 ..Default::default()
653 });
654 assert!(installer.build_limitation_message(&distro).is_none());
655 }
656
657 #[test]
658 fn test_limitation_message_arch_is_some() {
659 let installer = MigraphxInstaller::with_defaults();
660 let distro = DistroFacade::from_info(DistroInfo {
661 id: "arch".to_string(),
662 family: DistroFamily::Arch,
663 pkg_manager: PackageManager::Pacman,
664 ..Default::default()
665 });
666 let msg = installer
667 .build_limitation_message(&distro)
668 .expect("Arch should have a limitation message");
669 assert!(
670 msg.contains("limited support"),
671 "Message should mention limited support: {msg}"
672 );
673 assert!(
674 msg.contains("Python bindings"),
675 "Message should mention Python bindings: {msg}"
676 );
677 assert!(
678 msg.contains("Arch"),
679 "Message should mention Arch context: {msg}"
680 );
681 }
682
683 #[test]
684 fn test_should_skip_pip_debian_is_false() {
685 let installer = MigraphxInstaller::with_defaults();
686 let distro = DistroFacade::from_info(DistroInfo {
687 id: "ubuntu".to_string(),
688 family: DistroFamily::Debian,
689 pkg_manager: PackageManager::Apt,
690 ..Default::default()
691 });
692 assert!(!installer.should_skip_pip_install(&distro));
693 }
694
695 #[test]
696 fn test_should_skip_pip_arch_is_true() {
697 let installer = MigraphxInstaller::with_defaults();
698 let distro = DistroFacade::from_info(DistroInfo {
699 id: "arch".to_string(),
700 family: DistroFamily::Arch,
701 pkg_manager: PackageManager::Pacman,
702 ..Default::default()
703 });
704 assert!(installer.should_skip_pip_install(&distro));
705 }
706
707 #[test]
708 fn test_arch_required_packages_no_dev() {
709 let installer = MigraphxInstaller::with_defaults();
710 let distro = DistroFacade::from_info(DistroInfo {
711 id: "cachyos".to_string(),
712 family: DistroFamily::Arch,
713 pkg_manager: PackageManager::Pacman,
714 ..Default::default()
715 });
716 let pkgs = installer.required_packages(&distro);
717 assert!(
718 pkgs.contains(&"migraphx"),
719 "Arch should have migraphx package"
720 );
721 assert!(
722 !pkgs.contains(&"migraphx-dev"),
723 "Arch should NOT have migraphx-dev (Debian-specific)"
724 );
725 assert!(
726 !pkgs.contains(&"python3-migraphx"),
727 "Arch should NOT have python3-migraphx (Debian-specific)"
728 );
729 }
730
731 #[test]
732 fn test_migraphx_support_display() {
733 assert_eq!(format!("{}", MigraphxSupport::Full), "full");
734 assert_eq!(format!("{}", MigraphxSupport::SystemOnly), "system-only");
735 assert_eq!(format!("{}", MigraphxSupport::Unavailable), "unavailable");
736 }
737}