Skip to main content

rusty_stack/installers/components/
rocm.rs

1//! ROCm installer — ports `scripts/install_rocm.sh`.
2//!
3//! Constructs correct apt/dnf/pacman commands per distro family, with
4//! channel selection (Legacy/Stable/Latest) producing correct version
5//! pins and repository URLs.
6//!
7//! # Validation Assertions
8//!
9//! - **VAL-INSTALL-001**: ROCm installer correct package commands per distro
10//! - **VAL-INSTALL-002**: ROCm channel selection correct version pins and repo URLs
11
12use crate::installers::common::DistroFacade;
13use std::fmt;
14
15// ===========================================================================
16// Types
17// ===========================================================================
18
19/// ROCm release channel.
20#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
21pub enum RocmChannel {
22    /// ROCm 6.4.3 — conservative compatibility target (Legacy).
23    Legacy,
24    /// ROCm 7.2.3 — prior production target for RDNA 3/4 (Stable).
25    Stable,
26    /// ROCm 7.2.4 — current production release (Latest, default).
27    Latest,
28}
29
30impl fmt::Display for RocmChannel {
31    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
32        match self {
33            RocmChannel::Legacy => write!(f, "legacy"),
34            RocmChannel::Stable => write!(f, "stable"),
35            RocmChannel::Latest => write!(f, "latest"),
36        }
37    }
38}
39
40impl RocmChannel {
41    /// Parse from a choice number (1-3) as used in the original script.
42    pub fn from_choice(choice: u8) -> Option<Self> {
43        match choice {
44            1 => Some(RocmChannel::Legacy),
45            2 => Some(RocmChannel::Stable),
46            3 => Some(RocmChannel::Latest),
47            _ => None,
48        }
49    }
50
51    /// Get the ROCm version string for this channel.
52    pub fn version(&self) -> &'static str {
53        match self {
54            RocmChannel::Legacy => "6.4.3",
55            RocmChannel::Stable => "7.2.3",
56            RocmChannel::Latest => "7.2.4",
57        }
58    }
59
60    /// Get the ROCm directory path component for this channel.
61    pub fn dir_path(&self) -> &'static str {
62        match self {
63            RocmChannel::Legacy => "6.4.3",
64            RocmChannel::Stable => "7.2.3",
65            RocmChannel::Latest => "7.2.4",
66        }
67    }
68
69    /// Get the package version string used in installer package names.
70    pub fn pkg_version(&self) -> &'static str {
71        match self {
72            RocmChannel::Legacy => "6.4.60403-1",
73            RocmChannel::Stable => "7.2.3.70203-1",
74            RocmChannel::Latest => "7.2.4.70204-1",
75        }
76    }
77
78    /// Get the major.minor version (e.g., "7.2") for repo URLs.
79    pub fn major_minor(&self) -> &'static str {
80        match self {
81            RocmChannel::Legacy => "6.4",
82            RocmChannel::Stable => "7.2",
83            RocmChannel::Latest => "7.2",
84        }
85    }
86
87    /// Get the version for this channel, with optional manifest override.
88    ///
89    /// If a manifest is loaded, uses the manifest version for the "rocm" component.
90    /// Falls back to the hardcoded version if no manifest or manifest doesn't contain rocm.
91    pub fn version_from_manifest(manifest: &crate::core::manifest::Manifest) -> String {
92        manifest
93            .components
94            .iter()
95            .find(|c| c.id == "rocm")
96            .map(|c| c.version.clone())
97            .unwrap_or_else(|| RocmChannel::Latest.version().to_string())
98    }
99
100    /// Get the package version for this channel, with optional manifest override.
101    ///
102    /// Constructs a package version string from the manifest version if available.
103    pub fn pkg_version_from_manifest(manifest: &crate::core::manifest::Manifest) -> String {
104        let version = Self::version_from_manifest(manifest);
105        if version == "6.4.3" {
106            return "6.4.60403-1".to_string();
107        }
108
109        // Convert "7.2.4" to "7.2.4.70204-1" format.
110        let parts: Vec<&str> = version.split('.').collect();
111        if parts.len() >= 3 {
112            let major: u64 = parts[0].parse().unwrap_or(7);
113            let minor: u64 = parts[1].parse().unwrap_or(2);
114            let patch: u64 = parts[2].parse().unwrap_or(0);
115            let build = format!("{major}{minor:02}{patch:02}");
116            format!("{}.{build}-1", version)
117        } else {
118            RocmChannel::Latest.pkg_version().to_string()
119        }
120    }
121}
122
123/// Installation type selection.
124#[derive(Debug, Clone, Copy, PartialEq, Eq)]
125pub enum RocmInstallType {
126    /// ROCm runtime + basic tools.
127    Standard,
128    /// ROCm runtime only.
129    Minimal,
130    /// ROCm + development tools + libraries.
131    Full,
132    /// Select specific components.
133    Custom,
134}
135
136impl fmt::Display for RocmInstallType {
137    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
138        match self {
139            RocmInstallType::Standard => write!(f, "standard"),
140            RocmInstallType::Minimal => write!(f, "minimal"),
141            RocmInstallType::Full => write!(f, "full"),
142            RocmInstallType::Custom => write!(f, "custom"),
143        }
144    }
145}
146
147/// Configuration for the ROCm installer.
148#[derive(Debug, Clone)]
149pub struct RocmConfig {
150    /// Selected ROCm channel.
151    pub channel: RocmChannel,
152    /// Installation type.
153    pub install_type: RocmInstallType,
154    /// Whether to force reinstall.
155    pub force_reinstall: bool,
156    /// Whether to run in dry-run mode.
157    pub dry_run: bool,
158}
159
160impl Default for RocmConfig {
161    fn default() -> Self {
162        Self {
163            channel: RocmChannel::Latest,
164            install_type: RocmInstallType::Standard,
165            force_reinstall: false,
166            dry_run: false,
167        }
168    }
169}
170
171/// Constructed command for a package manager operation.
172#[derive(Debug, Clone, PartialEq, Eq)]
173pub struct PackageCommand {
174    /// The program to run (e.g., "sudo", "apt-get").
175    pub program: String,
176    /// Arguments to pass.
177    pub args: Vec<String>,
178}
179
180impl PackageCommand {
181    /// Format as a shell command string.
182    pub fn to_command_string(&self) -> String {
183        if self.args.is_empty() {
184            self.program.clone()
185        } else {
186            format!("{} {}", self.program, self.args.join(" "))
187        }
188    }
189}
190
191/// Constructed repository configuration for ROCm.
192#[derive(Debug, Clone)]
193pub struct RepoConfig {
194    /// Repository type (e.g., "ubuntu", "rhel", "sle").
195    pub repo_type: String,
196    /// Repository URL.
197    pub url: String,
198    /// GPG key URL.
199    pub gpg_key_url: String,
200    /// Target file path for the repo config.
201    pub file_path: String,
202}
203
204/// The ROCm installer.
205pub struct RocmInstaller {
206    config: RocmConfig,
207}
208
209impl RocmInstaller {
210    /// Create a new ROCm installer with the given config.
211    pub fn new(config: RocmConfig) -> Self {
212        Self { config }
213    }
214
215    /// Create with default config (Latest channel, Standard install).
216    pub fn with_defaults() -> Self {
217        Self::new(RocmConfig::default())
218    }
219
220    /// Get the ROCm channel.
221    pub fn channel(&self) -> RocmChannel {
222        self.config.channel
223    }
224
225    // -----------------------------------------------------------------------
226    // Repository URL construction (VAL-INSTALL-002)
227    // -----------------------------------------------------------------------
228
229    /// Get the amdgpu/ROCm repo base URL for the given distro and channel.
230    ///
231    /// For apt-based distros, the URL is:
232    /// `https://repo.radeon.com/amdgpu/{major_minor}/ubuntu/{codename}`
233    ///
234    /// For RHEL-based distros:
235    /// `https://repo.radeon.com/amdgpu/{major_minor}/rhel/{rhel_version}`
236    ///
237    /// For SUSE:
238    /// `https://repo.radeon.com/amdgpu/{major_minor}/sle/{sles_version}`
239    pub fn repo_base_url(&self, distro: &DistroFacade) -> String {
240        let major_minor = self.config.channel.major_minor();
241        if distro.uses_apt() {
242            let codename = if distro.codename().is_empty() {
243                "noble"
244            } else {
245                distro.codename()
246            };
247            format!("https://repo.radeon.com/amdgpu/{major_minor}/ubuntu/{codename}")
248        } else if distro.uses_dnf() || distro.uses_yum() {
249            let rhel_version = Self::rhel_version(distro);
250            format!("https://repo.radeon.com/amdgpu/{major_minor}/rhel/{rhel_version}")
251        } else if distro.uses_zypper() {
252            format!("https://repo.radeon.com/amdgpu/{major_minor}/sle/15.7")
253        } else {
254            // Fallback for Arch and others
255            format!("https://repo.radeon.com/amdgpu/{major_minor}/ubuntu/noble")
256        }
257    }
258
259    /// Get the ROCm core repo URL for apt-based distros.
260    pub fn rocm_core_repo_url(&self, _distro: &DistroFacade) -> String {
261        let major_minor = self.config.channel.major_minor();
262        format!("https://repo.radeon.com/rocm/apt/{major_minor}")
263    }
264
265    /// Get the GPG key URL.
266    pub fn gpg_key_url(&self) -> &'static str {
267        "https://repo.radeon.com/rocm/rocm.gpg.key"
268    }
269
270    /// Derive the RHEL major version from distro info.
271    fn rhel_version(distro: &DistroFacade) -> &'static str {
272        let v = distro.version();
273        if v.starts_with("10") {
274            "10"
275        } else if v.starts_with("9") {
276            "9"
277        } else if v.starts_with("8") {
278            "8"
279        } else {
280            "9"
281        }
282    }
283
284    // -----------------------------------------------------------------------
285    // Package command construction (VAL-INSTALL-001)
286    // -----------------------------------------------------------------------
287
288    /// Construct the amdgpu-install package download URL for apt-based distros.
289    pub fn amdgpu_install_deb_url(&self) -> String {
290        let release_dir = self.config.channel.dir_path();
291        let pkg_ver = self.config.channel.pkg_version();
292        format!(
293            "https://repo.radeon.com/amdgpu-install/{release_dir}/ubuntu/noble/amdgpu-install_{pkg_ver}_all.deb"
294        )
295    }
296
297    /// Construct the amdgpu-install RPM download URL.
298    pub fn amdgpu_install_rpm_url(&self, rhel_version: &str) -> String {
299        let release_dir = self.config.channel.dir_path();
300        let pkg_ver = self.config.channel.pkg_version();
301        format!(
302            "https://repo.radeon.com/amdgpu-install/{release_dir}/rhel/{rhel_version}/amdgpu-install-{pkg_ver}.el{rhel_version}.noarch.rpm"
303        )
304    }
305
306    /// Construct the amdgpu-install SLES RPM download URL.
307    pub fn amdgpu_install_sles_rpm_url(&self) -> String {
308        let release_dir = self.config.channel.dir_path();
309        let pkg_ver = self.config.channel.pkg_version();
310        format!(
311            "https://repo.radeon.com/amdgpu-install/{release_dir}/sle/15.7/amdgpu-install-{pkg_ver}.noarch.rpm"
312        )
313    }
314
315    /// Construct the apt-get install command for amdgpu-install.
316    pub fn apt_install_commands(&self) -> Vec<PackageCommand> {
317        let deb_url = self.amdgpu_install_deb_url();
318        vec![
319            // Download the .deb
320            PackageCommand {
321                program: "wget".to_string(),
322                args: vec!["-q".to_string(), deb_url.clone()],
323            },
324            // Install it
325            PackageCommand {
326                program: "sudo".to_string(),
327                args: vec![
328                    "dpkg".to_string(),
329                    "-i".to_string(),
330                    format!(
331                        "amdgpu-install_{}_all.deb",
332                        self.config.channel.pkg_version()
333                    ),
334                ],
335            },
336            // Update and install ROCm packages
337            PackageCommand {
338                program: "sudo".to_string(),
339                args: vec!["apt-get".to_string(), "update".to_string()],
340            },
341            // Install ROCm metapackage based on install type
342            self.apt_rocm_metapackage_command(),
343        ]
344    }
345
346    /// Get the apt metapackage install command for the selected install type.
347    fn apt_rocm_metapackage_command(&self) -> PackageCommand {
348        let metapkg = match self.config.install_type {
349            RocmInstallType::Standard => "rocm-libs",
350            RocmInstallType::Minimal => "rocm-core",
351            RocmInstallType::Full => "rocm-dev",
352            RocmInstallType::Custom => "rocm-libs",
353        };
354        PackageCommand {
355            program: "sudo".to_string(),
356            args: vec![
357                "apt-get".to_string(),
358                "install".to_string(),
359                "-y".to_string(),
360                metapkg.to_string(),
361            ],
362        }
363    }
364
365    /// Construct the dnf install commands for ROCm.
366    pub fn dnf_install_commands(&self, rhel_version: &str) -> Vec<PackageCommand> {
367        let rpm_url = self.amdgpu_install_rpm_url(rhel_version);
368        vec![
369            PackageCommand {
370                program: "sudo".to_string(),
371                args: vec![
372                    "dnf".to_string(),
373                    "install".to_string(),
374                    "-y".to_string(),
375                    rpm_url,
376                ],
377            },
378            self.dnf_rocm_metapackage_command(),
379        ]
380    }
381
382    /// Get the dnf metapackage install command.
383    fn dnf_rocm_metapackage_command(&self) -> PackageCommand {
384        let metapkg = match self.config.install_type {
385            RocmInstallType::Standard => "rocm-libs",
386            RocmInstallType::Minimal => "rocm-core",
387            RocmInstallType::Full => "rocm-dev",
388            RocmInstallType::Custom => "rocm-libs",
389        };
390        PackageCommand {
391            program: "sudo".to_string(),
392            args: vec![
393                "dnf".to_string(),
394                "install".to_string(),
395                "-y".to_string(),
396                metapkg.to_string(),
397            ],
398        }
399    }
400
401    /// Construct the pacman install commands for ROCm on Arch.
402    ///
403    /// Arch uses AUR packages (yay/paru) for ROCm.
404    pub fn pacman_install_commands(&self, aur_helper: &str) -> Vec<PackageCommand> {
405        let packages = self.pacman_rocm_packages();
406        let mut args = vec![
407            "-S".to_string(),
408            "--needed".to_string(),
409            "--noconfirm".to_string(),
410        ];
411        args.extend(packages);
412
413        vec![
414            PackageCommand {
415                program: "sudo".to_string(),
416                args: vec![aur_helper.to_string()],
417            },
418            PackageCommand {
419                program: aur_helper.to_string(),
420                args,
421            },
422        ]
423    }
424
425    /// Get the list of ROCm AUR packages for pacman-based installs.
426    pub fn pacman_rocm_packages(&self) -> Vec<String> {
427        let mut pkgs = vec![
428            "rocm-hip-sdk".to_string(),
429            "rocminfo".to_string(),
430            "rocm-smi-lib".to_string(),
431            "hipblas".to_string(),
432            "rocblas".to_string(),
433            "hipsparselt".to_string(),
434        ];
435        // Add extra packages for Stable/Latest channels
436        if self.config.channel != RocmChannel::Legacy {
437            pkgs.push("rocm-opencl-sdk".to_string());
438            pkgs.push("rccl".to_string());
439        }
440        pkgs
441    }
442
443    /// Construct the zypper install commands for ROCm on SUSE.
444    pub fn zypper_install_commands(&self) -> Vec<PackageCommand> {
445        let rpm_url = self.amdgpu_install_sles_rpm_url();
446        vec![
447            PackageCommand {
448                program: "sudo".to_string(),
449                args: vec![
450                    "zypper".to_string(),
451                    "--non-interactive".to_string(),
452                    "install".to_string(),
453                    rpm_url,
454                ],
455            },
456            PackageCommand {
457                program: "sudo".to_string(),
458                args: vec![
459                    "zypper".to_string(),
460                    "--non-interactive".to_string(),
461                    "install".to_string(),
462                    "-y".to_string(),
463                    "rocm-libs".to_string(),
464                ],
465            },
466        ]
467    }
468
469    /// Get the repository configuration for apt-based distros.
470    pub fn apt_repo_config(&self, distro: &DistroFacade) -> RepoConfig {
471        let codename = if distro.codename().is_empty() {
472            "noble"
473        } else {
474            distro.codename()
475        };
476        let major_minor = self.config.channel.major_minor();
477        RepoConfig {
478            repo_type: "apt".to_string(),
479            url: format!("deb [signed-by=/etc/apt/keyrings/rocm.gpg] https://repo.radeon.com/rocm/apt/{major_minor} {codename} main"),
480            gpg_key_url: "https://repo.radeon.com/rocm/rocm.gpg.key".to_string(),
481            file_path: "/etc/apt/sources.list.d/rocm.list".to_string(),
482        }
483    }
484
485    /// Get the amdgpu repo configuration for apt-based distros.
486    pub fn amdgpu_apt_repo_config(&self, distro: &DistroFacade) -> RepoConfig {
487        let codename = if distro.codename().is_empty() {
488            "noble"
489        } else {
490            distro.codename()
491        };
492        let major_minor = self.config.channel.major_minor();
493        RepoConfig {
494            repo_type: "apt".to_string(),
495            url: format!("deb [signed-by=/etc/apt/keyrings/rocm.gpg] https://repo.radeon.com/amdgpu/{major_minor}/ubuntu/{codename} {codename} main"),
496            gpg_key_url: "https://repo.radeon.com/rocm/rocm.gpg.key".to_string(),
497            file_path: "/etc/apt/sources.list.d/amdgpu.list".to_string(),
498        }
499    }
500
501    /// Get the ROCm preference pin configuration for apt.
502    pub fn apt_pin_config(&self) -> (String, String) {
503        (
504            "Package: *\nPin: release o=repo.radeon.com\nPin-Priority: 600\n".to_string(),
505            "/etc/apt/preferences.d/rocm-pin-600".to_string(),
506        )
507    }
508
509    /// Build a command to check if a single package is installed on Arch/pacman.
510    ///
511    /// Returns a `PackageCommand` that runs `pacman -Qi <package>`.
512    /// Exit code 0 means installed, non-zero means not installed.
513    pub fn build_pacman_check_installed_command(package: &str) -> PackageCommand {
514        PackageCommand {
515            program: "pacman".to_string(),
516            args: vec!["-Qi".to_string(), package.to_string()],
517        }
518    }
519
520    /// Filter out packages that are already installed on Arch/pacman systems.
521    ///
522    /// For each package in `packages`, runs `pacman -Qi <pkg>` to check if
523    /// it's installed. Returns only the packages that are NOT yet installed.
524    /// In dry-run mode, returns all packages (no checks performed).
525    pub fn filter_already_installed_pacman(packages: &[String]) -> Vec<String> {
526        use std::process::Command;
527
528        packages
529            .iter()
530            .filter(|pkg| {
531                let status = Command::new("pacman").args(["-Qi", pkg.as_str()]).output();
532                match status {
533                    Ok(output) => !output.status.success(),
534                    Err(_) => true, // If pacman fails to run, assume not installed
535                }
536            })
537            .cloned()
538            .collect()
539    }
540
541    /// Check if command output indicates a package is already up to date.
542    ///
543    /// yay/pacman output patterns that should be treated as success:
544    /// - `warning: <pkg> is up to date -- skipping`
545    /// - `<pkg> is up to date -- skipping`
546    /// - `there is nothing to do`
547    /// - `already installed`
548    /// - `.rc files restored` (post-install message, not an error)
549    pub fn is_up_to_date_output(output: &str) -> bool {
550        let lower = output.to_lowercase();
551        // yay/pacman "up to date" patterns
552        lower.contains("up to date -- skipping")
553            || lower.contains("up to date, skipping")
554            || lower.contains("up to date -- reinstalling")
555            || lower.contains("there is nothing to do")
556            || lower.contains("already installed")
557            // pacman -S on already-installed package
558            || lower.contains("warning:") && lower.contains("up to date")
559    }
560}
561
562// ===========================================================================
563// Tests
564// ===========================================================================
565
566#[cfg(test)]
567mod tests {
568    use super::*;
569    use crate::platform::detection::{DistroFamily, DistroInfo, PackageManager};
570
571    fn ubuntu_distro() -> DistroFacade {
572        DistroFacade::from_info(DistroInfo {
573            id: "ubuntu".to_string(),
574            name: "Ubuntu 24.04 LTS".to_string(),
575            version: "24.04".to_string(),
576            codename: "noble".to_string(),
577            family: DistroFamily::Debian,
578            pkg_manager: PackageManager::Apt,
579            id_like: "debian".to_string(),
580        })
581    }
582
583    fn fedora_distro() -> DistroFacade {
584        DistroFacade::from_info(DistroInfo {
585            id: "fedora".to_string(),
586            name: "Fedora Linux 41".to_string(),
587            version: "41".to_string(),
588            codename: String::new(),
589            family: DistroFamily::Rhel,
590            pkg_manager: PackageManager::Dnf,
591            id_like: "fedora".to_string(),
592        })
593    }
594
595    fn rhel9_distro() -> DistroFacade {
596        DistroFacade::from_info(DistroInfo {
597            id: "rhel".to_string(),
598            name: "Red Hat Enterprise Linux 9".to_string(),
599            version: "9.3".to_string(),
600            codename: String::new(),
601            family: DistroFamily::Rhel,
602            pkg_manager: PackageManager::Dnf,
603            id_like: "rhel".to_string(),
604        })
605    }
606
607    fn suse_distro() -> DistroFacade {
608        DistroFacade::from_info(DistroInfo {
609            id: "opensuse-leap".to_string(),
610            name: "openSUSE Leap 15.7".to_string(),
611            version: "15.7".to_string(),
612            codename: String::new(),
613            family: DistroFamily::Suse,
614            pkg_manager: PackageManager::Zypper,
615            id_like: "suse".to_string(),
616        })
617    }
618
619    // --- VAL-INSTALL-002: ROCm channel selection correct version pins ---
620
621    #[test]
622    fn test_channel_legacy_version() {
623        let ch = RocmChannel::Legacy;
624        assert_eq!(ch.version(), "6.4.3");
625        assert_eq!(ch.pkg_version(), "6.4.60403-1");
626        assert_eq!(ch.major_minor(), "6.4");
627    }
628
629    #[test]
630    fn test_channel_stable_version() {
631        let ch = RocmChannel::Stable;
632        assert_eq!(ch.version(), "7.2.3");
633        assert_eq!(ch.pkg_version(), "7.2.3.70203-1");
634        assert_eq!(ch.major_minor(), "7.2");
635    }
636
637    #[test]
638    fn test_channel_latest_version() {
639        let ch = RocmChannel::Latest;
640        assert_eq!(ch.version(), "7.2.4");
641        assert_eq!(ch.pkg_version(), "7.2.4.70204-1");
642        assert_eq!(ch.major_minor(), "7.2");
643    }
644
645    #[test]
646    fn test_channel_from_choice() {
647        assert_eq!(RocmChannel::from_choice(1), Some(RocmChannel::Legacy));
648        assert_eq!(RocmChannel::from_choice(2), Some(RocmChannel::Stable));
649        assert_eq!(RocmChannel::from_choice(3), Some(RocmChannel::Latest));
650        assert_eq!(RocmChannel::from_choice(0), None);
651        assert_eq!(RocmChannel::from_choice(4), None);
652    }
653
654    #[test]
655    fn test_pkg_version_from_manifest_latest() {
656        let manifest = crate::core::manifest::Manifest {
657            schema_version: 2,
658            sequence: 1,
659            generated_at: "2026-05-31T00:00:00Z".to_string(),
660            expires_at: None,
661            min_runtime_version: String::new(),
662            components: vec![crate::core::manifest::ManifestComponent {
663                id: "rocm".to_string(),
664                version: "7.2.4".to_string(),
665                script: "scripts/install_rocm.sh".to_string(),
666                category: crate::core::Category::Foundation,
667                validation_tier: crate::core::ValidationTier::Validated,
668                min_rocm_version: String::new(),
669                dependencies: vec![],
670                compatible_channels: vec!["latest".to_string()],
671            }],
672            signature: None,
673        };
674
675        assert_eq!(
676            RocmChannel::pkg_version_from_manifest(&manifest),
677            "7.2.4.70204-1"
678        );
679    }
680
681    #[test]
682    fn test_pkg_version_from_manifest_legacy() {
683        let manifest = crate::core::manifest::Manifest {
684            schema_version: 2,
685            sequence: 1,
686            generated_at: "2026-05-31T00:00:00Z".to_string(),
687            expires_at: None,
688            min_runtime_version: String::new(),
689            components: vec![crate::core::manifest::ManifestComponent {
690                id: "rocm".to_string(),
691                version: "6.4.3".to_string(),
692                script: "scripts/install_rocm.sh".to_string(),
693                category: crate::core::Category::Foundation,
694                validation_tier: crate::core::ValidationTier::Validated,
695                min_rocm_version: String::new(),
696                dependencies: vec![],
697                compatible_channels: vec!["legacy".to_string()],
698            }],
699            signature: None,
700        };
701
702        assert_eq!(
703            RocmChannel::pkg_version_from_manifest(&manifest),
704            "6.4.60403-1"
705        );
706    }
707
708    // --- VAL-INSTALL-001: ROCm installer correct package commands ---
709
710    #[test]
711    fn test_apt_install_commands_latest() {
712        let installer = RocmInstaller::new(RocmConfig {
713            channel: RocmChannel::Latest,
714            install_type: RocmInstallType::Standard,
715            ..Default::default()
716        });
717        let cmds = installer.apt_install_commands();
718        assert_eq!(cmds.len(), 4);
719
720        // wget download
721        assert_eq!(cmds[0].program, "wget");
722        assert!(cmds[0]
723            .args
724            .iter()
725            .any(|a| a.contains("7.2.4/ubuntu/noble")));
726        assert!(cmds[0].args.iter().any(|a| a.contains("7.2.4.70204-1")));
727        assert_eq!(cmds[1].program, "sudo");
728        assert!(cmds[1].args.contains(&"dpkg".to_string()));
729
730        // apt-get update
731        assert_eq!(cmds[2].program, "sudo");
732        assert!(cmds[2].args.contains(&"update".to_string()));
733
734        // rocm-libs metapackage
735        assert_eq!(cmds[3].program, "sudo");
736        assert!(cmds[3].args.contains(&"rocm-libs".to_string()));
737    }
738
739    #[test]
740    fn test_apt_install_commands_full() {
741        let installer = RocmInstaller::new(RocmConfig {
742            channel: RocmChannel::Stable,
743            install_type: RocmInstallType::Full,
744            ..Default::default()
745        });
746        let cmds = installer.apt_install_commands();
747        let meta_cmd = &cmds[3];
748        assert!(meta_cmd.args.contains(&"rocm-dev".to_string()));
749    }
750
751    #[test]
752    fn test_dnf_install_commands() {
753        let installer = RocmInstaller::new(RocmConfig {
754            channel: RocmChannel::Latest,
755            install_type: RocmInstallType::Standard,
756            ..Default::default()
757        });
758        let cmds = installer.dnf_install_commands("9");
759        assert_eq!(cmds.len(), 2);
760
761        // dnf install RPM
762        assert_eq!(cmds[0].program, "sudo");
763        assert!(cmds[0].args.iter().any(|a| a.contains("7.2.4/rhel/9")));
764        assert!(cmds[0].args.iter().any(|a| a.contains("7.2.4.70204-1")));
765        assert!(cmds[0].args.iter().any(|a| a.contains("el9")));
766
767        // rocm-libs metapackage
768        assert_eq!(cmds[1].program, "sudo");
769        assert!(cmds[1].args.contains(&"rocm-libs".to_string()));
770    }
771
772    #[test]
773    fn test_pacman_install_commands() {
774        let installer = RocmInstaller::new(RocmConfig {
775            channel: RocmChannel::Latest,
776            install_type: RocmInstallType::Standard,
777            ..Default::default()
778        });
779        let _cmds = installer.pacman_install_commands("yay");
780        // Should have packages for Latest channel
781        let pkgs = installer.pacman_rocm_packages();
782        assert!(pkgs.contains(&"rocm-hip-sdk".to_string()));
783        assert!(pkgs.contains(&"rocminfo".to_string()));
784        assert!(pkgs.contains(&"rocm-opencl-sdk".to_string())); // Latest gets extra
785        assert!(pkgs.contains(&"rccl".to_string())); // Latest gets extra
786    }
787
788    #[test]
789    fn test_pacman_legacy_no_extras() {
790        let installer = RocmInstaller::new(RocmConfig {
791            channel: RocmChannel::Legacy,
792            install_type: RocmInstallType::Standard,
793            ..Default::default()
794        });
795        let pkgs = installer.pacman_rocm_packages();
796        assert!(!pkgs.contains(&"rocm-opencl-sdk".to_string()));
797        assert!(!pkgs.contains(&"rccl".to_string()));
798    }
799
800    #[test]
801    fn test_zypper_install_commands() {
802        let installer = RocmInstaller::new(RocmConfig {
803            channel: RocmChannel::Latest,
804            install_type: RocmInstallType::Standard,
805            ..Default::default()
806        });
807        let cmds = installer.zypper_install_commands();
808        assert_eq!(cmds.len(), 2);
809        assert_eq!(cmds[0].program, "sudo");
810        assert!(cmds[0].args.iter().any(|a| a.contains("zypper")));
811        assert!(cmds[0].args.iter().any(|a| a.contains("7.2.4/sle/15.7")));
812        assert!(cmds[0].args.iter().any(|a| a.contains("7.2.4.70204-1")));
813    }
814
815    // --- Repo URL construction ---
816
817    #[test]
818    fn test_repo_base_url_ubuntu() {
819        let installer = RocmInstaller::new(RocmConfig {
820            channel: RocmChannel::Latest,
821            ..Default::default()
822        });
823        let distro = ubuntu_distro();
824        let url = installer.repo_base_url(&distro);
825        assert_eq!(url, "https://repo.radeon.com/amdgpu/7.2/ubuntu/noble");
826    }
827
828    #[test]
829    fn test_repo_base_url_rhel() {
830        let installer = RocmInstaller::new(RocmConfig {
831            channel: RocmChannel::Latest,
832            ..Default::default()
833        });
834        let distro = rhel9_distro();
835        let url = installer.repo_base_url(&distro);
836        assert_eq!(url, "https://repo.radeon.com/amdgpu/7.2/rhel/9");
837    }
838
839    #[test]
840    fn test_repo_base_url_fedora() {
841        let installer = RocmInstaller::new(RocmConfig {
842            channel: RocmChannel::Stable,
843            ..Default::default()
844        });
845        let distro = fedora_distro();
846        let url = installer.repo_base_url(&distro);
847        // Fedora version "41" doesn't match 8/9/10, defaults to 9
848        assert_eq!(url, "https://repo.radeon.com/amdgpu/7.2/rhel/9");
849    }
850
851    #[test]
852    fn test_repo_base_url_suse() {
853        let installer = RocmInstaller::new(RocmConfig {
854            channel: RocmChannel::Latest,
855            ..Default::default()
856        });
857        let distro = suse_distro();
858        let url = installer.repo_base_url(&distro);
859        assert_eq!(url, "https://repo.radeon.com/amdgpu/7.2/sle/15.7");
860    }
861
862    #[test]
863    fn test_amdgpu_install_deb_url() {
864        let installer = RocmInstaller::new(RocmConfig {
865            channel: RocmChannel::Latest,
866            ..Default::default()
867        });
868        let url = installer.amdgpu_install_deb_url();
869        assert!(url.contains("7.2.4/ubuntu/noble"));
870        assert!(url.contains("7.2.4.70204-1"));
871        assert!(url.contains("ubuntu/noble"));
872        assert!(url.ends_with(".deb"));
873    }
874
875    #[test]
876    fn test_amdgpu_install_rpm_url() {
877        let installer = RocmInstaller::new(RocmConfig {
878            channel: RocmChannel::Latest,
879            ..Default::default()
880        });
881        let url = installer.amdgpu_install_rpm_url("9");
882        assert!(url.contains("7.2.4/rhel/9"));
883        assert!(url.contains("7.2.4.70204-1"));
884        assert!(url.contains("rhel/9"));
885        assert!(url.ends_with(".noarch.rpm"));
886    }
887
888    #[test]
889    fn test_apt_repo_config() {
890        let installer = RocmInstaller::new(RocmConfig {
891            channel: RocmChannel::Latest,
892            ..Default::default()
893        });
894        let distro = ubuntu_distro();
895        let repo = installer.apt_repo_config(&distro);
896        assert_eq!(repo.file_path, "/etc/apt/sources.list.d/rocm.list");
897        assert!(repo.url.contains("rocm/apt/7.2"));
898        assert!(repo.url.contains("noble"));
899    }
900
901    #[test]
902    fn test_amdgpu_apt_repo_config() {
903        let installer = RocmInstaller::new(RocmConfig {
904            channel: RocmChannel::Latest,
905            ..Default::default()
906        });
907        let distro = ubuntu_distro();
908        let repo = installer.amdgpu_apt_repo_config(&distro);
909        assert_eq!(repo.file_path, "/etc/apt/sources.list.d/amdgpu.list");
910        assert!(repo.url.contains("amdgpu/7.2"));
911    }
912
913    #[test]
914    fn test_apt_pin_config() {
915        let installer = RocmInstaller::with_defaults();
916        let (content, path) = installer.apt_pin_config();
917        assert_eq!(path, "/etc/apt/preferences.d/rocm-pin-600");
918        assert!(content.contains("Pin-Priority: 600"));
919    }
920
921    // --- Cross-channel parametric tests ---
922
923    #[test]
924    fn test_all_channels_version_consistency() {
925        for choice in [1u8, 2, 3] {
926            let ch = RocmChannel::from_choice(choice).unwrap();
927            let config = RocmConfig {
928                channel: ch,
929                ..Default::default()
930            };
931            let installer = RocmInstaller::new(config);
932
933            // Verify deb URL contains correct package version
934            let deb_url = installer.amdgpu_install_deb_url();
935            assert!(
936                deb_url.contains(ch.pkg_version()),
937                "deb URL for {:?} should contain pkg_version {}",
938                ch,
939                ch.pkg_version()
940            );
941
942            // Verify repo URL contains major_minor
943            let distro = ubuntu_distro();
944            let repo_url = installer.repo_base_url(&distro);
945            assert!(
946                repo_url.contains(ch.major_minor()),
947                "repo URL for {:?} should contain major_minor {}",
948                ch,
949                ch.major_minor()
950            );
951        }
952    }
953
954    #[test]
955    fn test_install_type_metapackages() {
956        for (itype, expected_pkg) in [
957            (RocmInstallType::Standard, "rocm-libs"),
958            (RocmInstallType::Minimal, "rocm-core"),
959            (RocmInstallType::Full, "rocm-dev"),
960            (RocmInstallType::Custom, "rocm-libs"),
961        ] {
962            let installer = RocmInstaller::new(RocmConfig {
963                install_type: itype,
964                ..Default::default()
965            });
966            let cmds = installer.apt_install_commands();
967            let meta_cmd = &cmds[3];
968            assert!(
969                meta_cmd.args.contains(&expected_pkg.to_string()),
970                "Install type {:?} should use metapackage {}",
971                itype,
972                expected_pkg
973            );
974        }
975    }
976
977    // --- Up-to-date output detection ---
978
979    #[test]
980    fn test_is_up_to_date_yay_warning() {
981        assert!(RocmInstaller::is_up_to_date_output(
982            "warning: rocminfo is up to date -- skipping"
983        ));
984    }
985
986    #[test]
987    fn test_is_up_to_date_yay_warning_case_insensitive() {
988        assert!(RocmInstaller::is_up_to_date_output(
989            "WARNING: rocminfo is UP TO DATE -- skipping"
990        ));
991    }
992
993    #[test]
994    fn test_is_up_to_date_nothing_to_do() {
995        assert!(RocmInstaller::is_up_to_date_output(
996            " there is nothing to do\n"
997        ));
998    }
999
1000    #[test]
1001    fn test_is_up_to_date_already_installed() {
1002        assert!(RocmInstaller::is_up_to_date_output(
1003            "package already installed"
1004        ));
1005    }
1006
1007    #[test]
1008    fn test_is_up_to_date_normal_output_is_false() {
1009        assert!(!RocmInstaller::is_up_to_date_output(
1010            "resolving dependencies...\nlooking for conflicting packages...\n"
1011        ));
1012    }
1013
1014    #[test]
1015    fn test_is_up_to_date_error_output_is_false() {
1016        assert!(!RocmInstaller::is_up_to_date_output(
1017            "error: failed to prepare transaction\nerror: target not found: rocminfo"
1018        ));
1019    }
1020
1021    #[test]
1022    fn test_is_up_to_date_empty_output_is_false() {
1023        assert!(!RocmInstaller::is_up_to_date_output(""));
1024    }
1025
1026    #[test]
1027    fn test_is_up_to_date_pacman_skipping() {
1028        assert!(RocmInstaller::is_up_to_date_output(
1029            "warning: rocm-hip-sdk is up to date -- skipping"
1030        ));
1031    }
1032
1033    // --- Pacman check-installed command construction ---
1034
1035    #[test]
1036    fn test_build_pacman_check_installed_command() {
1037        let cmd = RocmInstaller::build_pacman_check_installed_command("rocminfo");
1038        assert_eq!(cmd.program, "pacman");
1039        assert_eq!(cmd.args, vec!["-Qi", "rocminfo"]);
1040    }
1041
1042    #[test]
1043    fn test_build_pacman_check_installed_command_complex_pkg() {
1044        let cmd = RocmInstaller::build_pacman_check_installed_command("rocm-hip-sdk");
1045        assert_eq!(cmd.program, "pacman");
1046        assert!(cmd.args.contains(&"rocm-hip-sdk".to_string()));
1047    }
1048}