Skip to main content

rusty_stack/installers/components/
migraphx_multi.rs

1//! MIGraphX installer — ports `scripts/install_migraphx_multi.sh`.
2//!
3//! Constructs correct pip install with version for MIGraphX Python bindings.
4//! Also installs system packages via package manager.
5//!
6//! # Arch Linux / CachyOS Handling
7//!
8//! On Arch-family distros, only the `migraphx` system package is available.
9//! The `migraphx-dev` and `python3-migraphx` packages are Debian/Ubuntu-specific.
10//! The pip `migraphx` Python wheel is also not available for Arch.
11//! The installer installs the system package only and skips pip install with
12//! a clear informational message.
13//!
14//! # Validation Assertion
15//!
16//! - **VAL-INSTALL-019**: MIGraphX correct pip command
17
18use crate::installers::common::DistroFacade;
19use crate::platform::detection::DistroFamily;
20
21// ===========================================================================
22// Types
23// ===========================================================================
24
25/// MIGraphX support level for a given distro.
26#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27pub enum MigraphxSupport {
28    /// Full support: system packages + Python bindings available.
29    Full,
30    /// Partial support: system package only, no Python bindings.
31    SystemOnly,
32    /// Not available on this distro.
33    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// ===========================================================================
47// Types
48// ===========================================================================
49
50/// A constructed shell command.
51#[derive(Debug, Clone, PartialEq, Eq)]
52pub struct ShellCommand {
53    /// The program to run.
54    pub program: String,
55    /// Arguments to pass.
56    pub args: Vec<String>,
57    /// Environment variables to set.
58    pub env: Vec<(String, String)>,
59}
60
61impl ShellCommand {
62    /// Format as a shell command string.
63    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/// Configuration for the MIGraphX installer.
84#[derive(Debug, Clone)]
85pub struct MigraphxConfig {
86    /// Python binary to use.
87    pub python_bin: String,
88    /// Whether to run in dry-run mode.
89    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
101/// The MIGraphX installer.
102pub struct MigraphxInstaller {
103    config: MigraphxConfig,
104}
105
106impl MigraphxInstaller {
107    /// Create a new MIGraphX installer with the given config.
108    pub fn new(config: MigraphxConfig) -> Self {
109        Self { config }
110    }
111
112    /// Create with default config.
113    pub fn with_defaults() -> Self {
114        Self::new(MigraphxConfig::default())
115    }
116
117    // -----------------------------------------------------------------------
118    // Package lists per distro (VAL-INSTALL-019)
119    // -----------------------------------------------------------------------
120
121    /// Get the required system packages for the given distro.
122    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    /// Get the optional system packages for the given distro.
130    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    /// Construct the package install command for the given distro.
138    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    /// Construct the package update command for the given distro.
195    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    /// Construct the pip install command for MIGraphX Python bindings.
235    ///
236    /// The original script installs system packages and verifies Python import.
237    /// The pip install is from the ROCm Python path.
238    ///
239    /// **Note:** On Arch-family distros, this command should NOT be executed
240    /// because the `migraphx` pip wheel is not available. Use
241    /// `is_available_on_distro()` to check first.
242    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    // -----------------------------------------------------------------------
257    // Distro availability (Arch-specific handling)
258    // -----------------------------------------------------------------------
259
260    /// Check the level of MIGraphX support on the given distro.
261    ///
262    /// - **Debian/Ubuntu**: Full support (system packages + Python bindings).
263    /// - **Arch/CachyOS**: System-only support (`migraphx` package available,
264    ///   but no `python3-migraphx` or pip wheel).
265    /// - **Others**: Assumed full support (will attempt standard install).
266    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    /// Build a human-readable message explaining MIGraphX limitations on the
274    /// given distro. Returns `None` if there are no limitations (full support).
275    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    /// Check whether the pip install step should be skipped on this distro.
295    ///
296    /// On Arch-family distros, the `migraphx` pip wheel does not exist and
297    /// attempting to install it will fail. This method returns `true` for
298    /// those distros so the caller can skip the pip step gracefully.
299    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
307// ===========================================================================
308// Functional runtime checks (detection + verification)
309// ===========================================================================
310//
311// On Arch/CachyOS the `migraphx` pip wheel does not exist — only the C++
312// toolchain ships (`/opt/rocm/bin/migraphx-driver` + libs). Detection and
313// verification therefore cannot rely on `import migraphx` alone; they must also
314// confirm the driver actually LOADS. A driver that fails to load its shared
315// libraries is NOT functional — the classic cause on a rolling-release distro
316// is a SONAME desync from a partial upgrade: migraphx is built against
317// libprotobuf.so.35.1.0 / libabsl_*.so.2605.0.0 but the system still has the
318// older libprotobuf.so.35.0.0 / older abseil. `sudo pacman -Syu` resolves it.
319
320/// Run `migraphx-driver --version`. Returns `Ok(())` if it loads, or
321/// `Err(missing_libs)` listing the shared libraries that failed to load (from
322/// `ldd`), so the failure is actionable rather than a vague "verify error".
323pub 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
337/// Boolean form of [`migraphx_driver_status`] for detection.
338pub fn migraphx_driver_functional() -> bool {
339    migraphx_driver_status().is_ok()
340}
341
342/// Run `migraphx-driver --version` and return the parsed semantic version, or
343/// `None` if the driver is missing/non-functional/has no parseable version.
344///
345/// This is the version-resolution counterpart to [`migraphx_driver_functional`]
346/// and the single owner of the `migraphx-driver --version` subprocess +
347/// semver parsing (used by `platform::registry::get_version`'s migraphx
348/// fallback). Keeping it here follows the module organization rule: the
349/// installers module owns migraphx verification, not `platform/`.
350pub 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
369/// Extract the first version-like token from the driver's `--version` output.
370///
371/// Accepts both three-part `MAJOR.MINOR.PATCH` (the common shape, e.g.
372/// `MIGraphX 2.15.0`) and two-part `MAJOR.MINOR` (some ROCm builds/drivers omit
373/// the patch component). Returns the version unchanged (no normalization) so a
374/// detected `2.12` stays `2.12`. Rejects malformed/non-numeric components.
375///
376/// A two-part form is accepted ONLY when there is NO third component. A token
377/// like `2.12.x` or `2.12.` HAS a third component that fails numeric validation,
378/// so the whole token is rejected (continue to the next token) rather than
379/// silently truncating to `2.12` — otherwise a malformed driver version would
380/// participate in registry/update comparisons as a legitimate version.
381fn 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        // `continue` (not `?`) so a non-version token (e.g. "MIGraphX", "Version:",
386        // "dirty") advances to the next token instead of returning None.
387        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            // Three-part MAJOR.MINOR.PATCH — require a NON-EMPTY numeric patch.
401            // (An empty patch from a trailing dot, e.g. "2.12.", is malformed.)
402            Some(pat) if !pat.is_empty() && pat.chars().all(|c| c.is_ascii_digit()) => {
403                return Some(format!("{maj}.{min}.{pat}"));
404            }
405            // A third component exists but is empty/non-numeric (e.g. "2.12.x",
406            // "2.12.") — malformed, do NOT truncate to 2.12; try the next token.
407            Some(_) => continue,
408            // No third component — two-part MAJOR.MINOR (preserve as-is).
409            None => return Some(format!("{maj}.{min}")),
410        }
411    }
412    None
413}
414
415/// Collect the `=> not found` shared libraries from `ldd migraphx-driver`.
416fn 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// ===========================================================================
431// Tests
432// ===========================================================================
433
434#[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        // Common three-part shape (the current driver output).
442        assert_eq!(
443            extract_driver_semver("MIGraphX Version: 2.15.0.20250912-dirty"),
444            Some("2.15.0".to_string())
445        );
446        // Two-part shape (some ROCm builds omit the patch component) — preserved
447        // as-is, no synthetic patch.
448        assert_eq!(
449            extract_driver_semver("MIGraphX 2.12"),
450            Some("2.12".to_string())
451        );
452        // Picks the first version-like token among noise.
453        assert_eq!(
454            extract_driver_semver("build foo 1.2.3 bar"),
455            Some("1.2.3".to_string())
456        );
457        // Rejects non-numeric components.
458        assert_eq!(extract_driver_semver("MIGraphX vX.Y.Z"), None);
459        assert_eq!(extract_driver_semver("no version here"), None);
460
461        // Malformed three-part tokens MUST NOT truncate to a two-part version.
462        // A third component that fails numeric validation rejects the whole token
463        // (continue to the next), so a malformed driver version can't masquerade
464        // as a legitimate 2.12 in registry/update comparisons.
465        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        // But a clean 2.12 (no third component) is still accepted.
476        assert_eq!(
477            extract_driver_semver("driver 2.12 build"),
478            Some("2.12".to_string())
479        );
480    }
481
482    // --- VAL-INSTALL-019: MIGraphX correct pip command ---
483
484    #[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    // --- Arch-specific availability tests ---
584
585    #[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}