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// Tests
309// ===========================================================================
310
311#[cfg(test)]
312mod tests {
313    use super::*;
314    use crate::platform::detection::{DistroInfo, PackageManager};
315
316    // --- VAL-INSTALL-019: MIGraphX correct pip command ---
317
318    #[test]
319    fn test_required_packages_debian() {
320        let installer = MigraphxInstaller::with_defaults();
321        let distro = DistroFacade::from_info(DistroInfo {
322            id: "ubuntu".to_string(),
323            family: DistroFamily::Debian,
324            pkg_manager: PackageManager::Apt,
325            ..Default::default()
326        });
327        let pkgs = installer.required_packages(&distro);
328        assert!(pkgs.contains(&"migraphx"));
329        assert!(pkgs.contains(&"migraphx-dev"));
330    }
331
332    #[test]
333    fn test_required_packages_arch() {
334        let installer = MigraphxInstaller::with_defaults();
335        let distro = DistroFacade::from_info(DistroInfo {
336            id: "arch".to_string(),
337            family: DistroFamily::Arch,
338            pkg_manager: PackageManager::Pacman,
339            ..Default::default()
340        });
341        let pkgs = installer.required_packages(&distro);
342        assert!(pkgs.contains(&"migraphx"));
343        assert!(!pkgs.contains(&"migraphx-dev"));
344    }
345
346    #[test]
347    fn test_optional_packages() {
348        let installer = MigraphxInstaller::with_defaults();
349        let distro = DistroFacade::from_info(DistroInfo {
350            id: "ubuntu".to_string(),
351            family: DistroFamily::Debian,
352            pkg_manager: PackageManager::Apt,
353            ..Default::default()
354        });
355        let pkgs = installer.optional_packages(&distro);
356        assert!(pkgs.contains(&"python3-migraphx"));
357        assert!(pkgs.contains(&"half"));
358    }
359
360    #[test]
361    fn test_package_install_debian() {
362        let installer = MigraphxInstaller::with_defaults();
363        let distro = DistroFacade::from_info(DistroInfo {
364            id: "ubuntu".to_string(),
365            family: DistroFamily::Debian,
366            pkg_manager: PackageManager::Apt,
367            ..Default::default()
368        });
369        let cmd = installer.build_package_install_command(&distro, &["migraphx", "migraphx-dev"]);
370        assert_eq!(cmd.program, "sudo");
371        assert!(cmd.args.contains(&"apt-get".to_string()));
372        assert!(cmd.args.contains(&"install".to_string()));
373        assert!(cmd.args.contains(&"migraphx".to_string()));
374        assert!(cmd.args.contains(&"migraphx-dev".to_string()));
375    }
376
377    #[test]
378    fn test_package_install_arch() {
379        let installer = MigraphxInstaller::with_defaults();
380        let distro = DistroFacade::from_info(DistroInfo {
381            id: "arch".to_string(),
382            family: DistroFamily::Arch,
383            pkg_manager: PackageManager::Pacman,
384            ..Default::default()
385        });
386        let cmd = installer.build_package_install_command(&distro, &["migraphx"]);
387        assert!(cmd.args.contains(&"pacman".to_string()));
388        assert!(cmd.args.contains(&"-S".to_string()));
389        assert!(cmd.args.contains(&"migraphx".to_string()));
390    }
391
392    #[test]
393    fn test_package_install_rhel() {
394        let installer = MigraphxInstaller::with_defaults();
395        let distro = DistroFacade::from_info(DistroInfo {
396            id: "fedora".to_string(),
397            family: DistroFamily::Rhel,
398            pkg_manager: PackageManager::Dnf,
399            ..Default::default()
400        });
401        let cmd = installer.build_package_install_command(&distro, &["migraphx", "migraphx-dev"]);
402        assert!(cmd.args.contains(&"dnf".to_string()));
403        assert!(cmd.args.contains(&"migraphx".to_string()));
404    }
405
406    #[test]
407    fn test_pip_install_command() {
408        let installer = MigraphxInstaller::with_defaults();
409        let cmd = installer.build_pip_install_command();
410        assert_eq!(cmd.program, "python3");
411        assert!(cmd.args.contains(&"-m".to_string()));
412        assert!(cmd.args.contains(&"pip".to_string()));
413        assert!(cmd.args.contains(&"install".to_string()));
414        assert!(cmd.args.contains(&"migraphx".to_string()));
415    }
416
417    // --- Arch-specific availability tests ---
418
419    #[test]
420    fn test_availability_debian_is_full() {
421        let installer = MigraphxInstaller::with_defaults();
422        let distro = DistroFacade::from_info(DistroInfo {
423            id: "ubuntu".to_string(),
424            family: DistroFamily::Debian,
425            pkg_manager: PackageManager::Apt,
426            ..Default::default()
427        });
428        assert_eq!(
429            installer.is_available_on_distro(&distro),
430            MigraphxSupport::Full
431        );
432    }
433
434    #[test]
435    fn test_availability_arch_is_system_only() {
436        let installer = MigraphxInstaller::with_defaults();
437        let distro = DistroFacade::from_info(DistroInfo {
438            id: "arch".to_string(),
439            family: DistroFamily::Arch,
440            pkg_manager: PackageManager::Pacman,
441            ..Default::default()
442        });
443        assert_eq!(
444            installer.is_available_on_distro(&distro),
445            MigraphxSupport::SystemOnly
446        );
447    }
448
449    #[test]
450    fn test_availability_cachyos_is_system_only() {
451        let installer = MigraphxInstaller::with_defaults();
452        let distro = DistroFacade::from_info(DistroInfo {
453            id: "cachyos".to_string(),
454            family: DistroFamily::Arch,
455            pkg_manager: PackageManager::Pacman,
456            ..Default::default()
457        });
458        assert_eq!(
459            installer.is_available_on_distro(&distro),
460            MigraphxSupport::SystemOnly
461        );
462    }
463
464    #[test]
465    fn test_availability_rhel_is_full() {
466        let installer = MigraphxInstaller::with_defaults();
467        let distro = DistroFacade::from_info(DistroInfo {
468            id: "fedora".to_string(),
469            family: DistroFamily::Rhel,
470            pkg_manager: PackageManager::Dnf,
471            ..Default::default()
472        });
473        assert_eq!(
474            installer.is_available_on_distro(&distro),
475            MigraphxSupport::Full
476        );
477    }
478
479    #[test]
480    fn test_limitation_message_debian_is_none() {
481        let installer = MigraphxInstaller::with_defaults();
482        let distro = DistroFacade::from_info(DistroInfo {
483            id: "ubuntu".to_string(),
484            family: DistroFamily::Debian,
485            pkg_manager: PackageManager::Apt,
486            ..Default::default()
487        });
488        assert!(installer.build_limitation_message(&distro).is_none());
489    }
490
491    #[test]
492    fn test_limitation_message_arch_is_some() {
493        let installer = MigraphxInstaller::with_defaults();
494        let distro = DistroFacade::from_info(DistroInfo {
495            id: "arch".to_string(),
496            family: DistroFamily::Arch,
497            pkg_manager: PackageManager::Pacman,
498            ..Default::default()
499        });
500        let msg = installer
501            .build_limitation_message(&distro)
502            .expect("Arch should have a limitation message");
503        assert!(
504            msg.contains("limited support"),
505            "Message should mention limited support: {msg}"
506        );
507        assert!(
508            msg.contains("Python bindings"),
509            "Message should mention Python bindings: {msg}"
510        );
511        assert!(
512            msg.contains("Arch"),
513            "Message should mention Arch context: {msg}"
514        );
515    }
516
517    #[test]
518    fn test_should_skip_pip_debian_is_false() {
519        let installer = MigraphxInstaller::with_defaults();
520        let distro = DistroFacade::from_info(DistroInfo {
521            id: "ubuntu".to_string(),
522            family: DistroFamily::Debian,
523            pkg_manager: PackageManager::Apt,
524            ..Default::default()
525        });
526        assert!(!installer.should_skip_pip_install(&distro));
527    }
528
529    #[test]
530    fn test_should_skip_pip_arch_is_true() {
531        let installer = MigraphxInstaller::with_defaults();
532        let distro = DistroFacade::from_info(DistroInfo {
533            id: "arch".to_string(),
534            family: DistroFamily::Arch,
535            pkg_manager: PackageManager::Pacman,
536            ..Default::default()
537        });
538        assert!(installer.should_skip_pip_install(&distro));
539    }
540
541    #[test]
542    fn test_arch_required_packages_no_dev() {
543        let installer = MigraphxInstaller::with_defaults();
544        let distro = DistroFacade::from_info(DistroInfo {
545            id: "cachyos".to_string(),
546            family: DistroFamily::Arch,
547            pkg_manager: PackageManager::Pacman,
548            ..Default::default()
549        });
550        let pkgs = installer.required_packages(&distro);
551        assert!(
552            pkgs.contains(&"migraphx"),
553            "Arch should have migraphx package"
554        );
555        assert!(
556            !pkgs.contains(&"migraphx-dev"),
557            "Arch should NOT have migraphx-dev (Debian-specific)"
558        );
559        assert!(
560            !pkgs.contains(&"python3-migraphx"),
561            "Arch should NOT have python3-migraphx (Debian-specific)"
562        );
563    }
564
565    #[test]
566    fn test_migraphx_support_display() {
567        assert_eq!(format!("{}", MigraphxSupport::Full), "full");
568        assert_eq!(format!("{}", MigraphxSupport::SystemOnly), "system-only");
569        assert_eq!(format!("{}", MigraphxSupport::Unavailable), "unavailable");
570    }
571}