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/// Classic (amdgpu-install) ROCm fallback for the Legacy channel.
20pub const LEGACY_FALLBACK_VERSION: &str = "7.2.3";
21/// TheRock multi-arch apt/rpm repo root (ROCm 7.14+).
22pub const THEROCK_REPO_ROOT: &str = "https://repo.amd.com/rocm/packages-multi-arch";
23/// TheRock multi-arch apt GPG key.
24pub const THEROCK_GPG_URL: &str = "https://repo.amd.com/rocm/packages-multi-arch/gpg/rocm.gpg";
25/// Fingerprint of AMD's multi-arch repo signing key (verified after download).
26pub const THEROCK_GPG_FINGERPRINT: &str = "D0F004A0025A1145C7807FCD0701EAC4D5E02107";
27/// Default runfile build id in `rocm-installer-<ver>-<build>.run`.
28pub const RUNFILE_BUILD_DEFAULT: &str = "7";
29/// TheRock multi-arch pip index (7.14.x).
30pub const THEROCK_PIP_INDEX: &str = "https://repo.amd.com/rocm/whl-multi-arch/";
31/// ROCm 10.x preview pip index.
32pub const PREVIEW_PIP_INDEX: &str = "https://stable.repo.amd.com/rocm/whl-next/";
33/// TheRock tarball root.
34pub const THEROCK_TARBALL_ROOT: &str = "https://repo.amd.com/rocm/tarball-multi-arch";
35/// Runfile installer root.
36pub const RUNFILE_ROOT: &str = "https://repo.radeon.com/installer/rocm-runfile-installer";
37
38/// ROCm release channel.
39#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
40pub enum RocmChannel {
41    /// ROCm 7.2.4 (7.2.3 fallback) — classic amdgpu-install line (Legacy).
42    Legacy,
43    /// ROCm 7.14.0 — TheRock (Stable).
44    Stable,
45    /// ROCm 7.14.1 — TheRock (Latest, default).
46    Latest,
47    /// ROCm 10.1.0 — TheRock Core SDK preview (pip/tarball only).
48    Preview,
49}
50
51impl fmt::Display for RocmChannel {
52    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
53        match self {
54            RocmChannel::Legacy => write!(f, "legacy"),
55            RocmChannel::Stable => write!(f, "stable"),
56            RocmChannel::Latest => write!(f, "latest"),
57            RocmChannel::Preview => write!(f, "preview"),
58        }
59    }
60}
61
62impl RocmChannel {
63    /// Parse from a choice number (1-4).
64    pub fn from_choice(choice: u8) -> Option<Self> {
65        match choice {
66            1 => Some(RocmChannel::Legacy),
67            2 => Some(RocmChannel::Stable),
68            3 => Some(RocmChannel::Latest),
69            4 => Some(RocmChannel::Preview),
70            _ => None,
71        }
72    }
73
74    /// Parse a channel name (`ROCM_CHANNEL`); unknown values yield `None`.
75    pub fn parse(name: &str) -> Option<Self> {
76        match name.trim().to_ascii_lowercase().as_str() {
77            "legacy" => Some(RocmChannel::Legacy),
78            "stable" => Some(RocmChannel::Stable),
79            "latest" => Some(RocmChannel::Latest),
80            "preview" => Some(RocmChannel::Preview),
81            _ => None,
82        }
83    }
84
85    /// Channel for a detected ROCm version string.
86    pub fn for_version(version: &str) -> Self {
87        let mut it = version.split('.');
88        let major: u32 = it.next().and_then(|v| v.parse().ok()).unwrap_or(0);
89        let minor: u32 = it.next().and_then(|v| v.parse().ok()).unwrap_or(0);
90        let patch: u32 = it.next().and_then(|v| v.parse().ok()).unwrap_or(0);
91        match (major, minor) {
92            (10.., _) => RocmChannel::Preview,
93            (7, 14..) if patch >= 1 || minor > 14 => RocmChannel::Latest,
94            (7, 14..) => RocmChannel::Stable,
95            _ => RocmChannel::Legacy,
96        }
97    }
98
99    /// True for TheRock-based releases (7.14+), which use the multi-arch repos.
100    pub fn is_therock(&self) -> bool {
101        !matches!(self, RocmChannel::Legacy)
102    }
103
104    /// Get the ROCm version string for this channel.
105    pub fn version(&self) -> &'static str {
106        match self {
107            RocmChannel::Legacy => "7.2.4",
108            RocmChannel::Stable => "7.14.0",
109            RocmChannel::Latest => "7.14.1",
110            RocmChannel::Preview => "10.1.0",
111        }
112    }
113
114    /// Get the ROCm directory path component for this channel.
115    pub fn dir_path(&self) -> &'static str {
116        self.version()
117    }
118
119    /// Get the classic package version string used in amdgpu-install names.
120    /// Only meaningful for the Legacy channel; TheRock uses `therock_pkg_revision`.
121    pub fn pkg_version(&self) -> &'static str {
122        match self {
123            RocmChannel::Legacy => "7.2.4.70204-1",
124            RocmChannel::Stable => "7.14.0",
125            RocmChannel::Latest => "7.14.1",
126            RocmChannel::Preview => "10.1.0",
127        }
128    }
129
130    /// Get the major.minor version (e.g., "7.14") for repo URLs and package names.
131    pub fn major_minor(&self) -> &'static str {
132        match self {
133            RocmChannel::Legacy => "7.2",
134            RocmChannel::Stable | RocmChannel::Latest => "7.14",
135            RocmChannel::Preview => "10.1",
136        }
137    }
138
139    /// Get the version for this channel, with optional manifest override.
140    ///
141    /// If a manifest is loaded, uses the manifest version for the "rocm" component.
142    /// Falls back to the hardcoded version if no manifest or manifest doesn't contain rocm.
143    pub fn version_from_manifest(manifest: &crate::core::manifest::Manifest) -> String {
144        manifest
145            .components
146            .iter()
147            .find(|c| c.id == "rocm")
148            .map(|c| c.version.clone())
149            .unwrap_or_else(|| RocmChannel::Latest.version().to_string())
150    }
151
152    /// Get the package version for this channel, with optional manifest override.
153    ///
154    /// Classic (<7.14) releases map `7.2.4` -> `7.2.4.70204-1`; TheRock releases
155    /// use the bare version.
156    pub fn pkg_version_from_manifest(manifest: &crate::core::manifest::Manifest) -> String {
157        let version = Self::version_from_manifest(manifest);
158        if version == "6.4.3" {
159            return "6.4.60403-1".to_string();
160        }
161        let parts: Vec<&str> = version.split('.').collect();
162        if parts.len() >= 3 {
163            let major: u64 = parts[0].parse().unwrap_or(7);
164            let minor: u64 = parts[1].parse().unwrap_or(2);
165            let patch: u64 = parts[2].parse().unwrap_or(0);
166            // Same classification as `for_version`, so 8.x/9.x stay classic.
167            if RocmChannel::for_version(&version).is_therock() {
168                return version;
169            }
170            let build = format!("{major}{minor:02}{patch:02}");
171            format!("{}.{build}-1", version)
172        } else {
173            RocmChannel::Latest.pkg_version().to_string()
174        }
175    }
176}
177
178/// Installation type selection.
179#[derive(Debug, Clone, Copy, PartialEq, Eq)]
180pub enum RocmInstallType {
181    /// ROCm runtime + basic tools.
182    Standard,
183    /// ROCm runtime only.
184    Minimal,
185    /// ROCm + development tools + libraries.
186    Full,
187    /// Select specific components.
188    Custom,
189}
190
191impl fmt::Display for RocmInstallType {
192    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
193        match self {
194            RocmInstallType::Standard => write!(f, "standard"),
195            RocmInstallType::Minimal => write!(f, "minimal"),
196            RocmInstallType::Full => write!(f, "full"),
197            RocmInstallType::Custom => write!(f, "custom"),
198        }
199    }
200}
201
202/// Configuration for the ROCm installer.
203#[derive(Debug, Clone)]
204pub struct RocmConfig {
205    /// Selected ROCm channel.
206    pub channel: RocmChannel,
207    /// Installation type.
208    pub install_type: RocmInstallType,
209    /// Whether to force reinstall.
210    pub force_reinstall: bool,
211    /// Whether to run in dry-run mode.
212    pub dry_run: bool,
213    /// Target GPU architecture (e.g. `gfx1100`) for TheRock per-arch packages.
214    pub gfx_arch: String,
215    /// Use the 7.2.3 classic build instead of 7.2.4 on the Legacy channel.
216    pub legacy_fallback: bool,
217}
218
219impl Default for RocmConfig {
220    fn default() -> Self {
221        Self {
222            channel: RocmChannel::Latest,
223            install_type: RocmInstallType::Standard,
224            force_reinstall: false,
225            dry_run: false,
226            gfx_arch: "gfx1100".to_string(),
227            legacy_fallback: false,
228        }
229    }
230}
231
232/// Constructed command for a package manager operation.
233#[derive(Debug, Clone, PartialEq, Eq)]
234pub struct PackageCommand {
235    /// The program to run (e.g., "sudo", "apt-get").
236    pub program: String,
237    /// Arguments to pass.
238    pub args: Vec<String>,
239}
240
241impl PackageCommand {
242    /// Format as a shell command string.
243    pub fn to_command_string(&self) -> String {
244        if self.args.is_empty() {
245            self.program.clone()
246        } else {
247            format!("{} {}", self.program, self.args.join(" "))
248        }
249    }
250}
251
252/// Constructed repository configuration for ROCm.
253#[derive(Debug, Clone)]
254pub struct RepoConfig {
255    /// Repository type (e.g., "ubuntu", "rhel", "sle").
256    pub repo_type: String,
257    /// Repository URL.
258    pub url: String,
259    /// GPG key URL.
260    pub gpg_key_url: String,
261    /// Target file path for the repo config.
262    pub file_path: String,
263}
264
265/// The ROCm installer.
266pub struct RocmInstaller {
267    config: RocmConfig,
268}
269
270impl RocmInstaller {
271    /// Create a new ROCm installer with the given config.
272    pub fn new(config: RocmConfig) -> Self {
273        Self { config }
274    }
275
276    /// Create with default config (Latest channel, Standard install).
277    pub fn with_defaults() -> Self {
278        Self::new(RocmConfig::default())
279    }
280
281    /// Get the ROCm channel.
282    pub fn channel(&self) -> RocmChannel {
283        self.config.channel
284    }
285
286    /// Effective ROCm version, honouring the Legacy 7.2.3 fallback.
287    pub fn effective_version(&self) -> &'static str {
288        if self.config.channel == RocmChannel::Legacy && self.config.legacy_fallback {
289            LEGACY_FALLBACK_VERSION
290        } else {
291            self.config.channel.version()
292        }
293    }
294
295    fn classic_release(&self) -> &'static str {
296        self.effective_version()
297    }
298
299    fn classic_pkg_version(&self) -> &'static str {
300        if self.config.channel == RocmChannel::Legacy && self.config.legacy_fallback {
301            "7.2.3.70203-1"
302        } else {
303            self.config.channel.pkg_version()
304        }
305    }
306
307    // -----------------------------------------------------------------------
308    // TheRock (7.14+) install methods
309    // -----------------------------------------------------------------------
310
311    /// Repo directory name for a distro on the TheRock multi-arch repo
312    /// (e.g. `ubuntu2404`, `debian12`, `rhel9`, `sles16`). `None` if unsupported.
313    pub fn therock_distro_dir(distro: &DistroFacade) -> Option<String> {
314        let v = distro.version();
315        let major = v.split('.').next().unwrap_or("");
316        if distro.uses_apt() {
317            if distro.id() == "debian" {
318                return match major {
319                    "12" | "13" => Some(format!("debian{major}")),
320                    _ => None,
321                };
322            }
323            let compact = v.replace('.', "");
324            match compact.as_str() {
325                "2204" | "2404" | "2604" => Some(format!("ubuntu{compact}")),
326                _ => match distro.codename() {
327                    "jammy" => Some("ubuntu2204".into()),
328                    "noble" => Some("ubuntu2404".into()),
329                    _ => None,
330                },
331            }
332        } else if distro.uses_dnf() || distro.uses_yum() {
333            match major {
334                "8" | "9" | "10" => Some(format!("rhel{major}")),
335                _ => None,
336            }
337        } else if distro.uses_zypper() {
338            match major {
339                "15" => Some("sles15".into()),
340                "16" => Some("sles16".into()),
341                _ => None,
342            }
343        } else {
344            None
345        }
346    }
347
348    /// Per-GPU metapackage name, e.g. `amdrocm7.14-gfx1100`.
349    pub fn therock_package(&self) -> String {
350        format!(
351            "amdrocm{}-{}",
352            self.config.channel.major_minor(),
353            self.config.gfx_arch
354        )
355    }
356
357    /// apt `Version` of the metapackage: `7.14.1` -> `7.14.1-*`.
358    ///
359    /// The glob is intentional: it pins the ROCm release but tracks the
360    /// repo's latest packaging revision (`-0`, `-1`, ...) of that release.
361    pub fn therock_apt_version_glob(&self) -> String {
362        format!("{}-*", self.config.channel.version())
363    }
364
365    /// Commands that configure the TheRock apt repo (key, source) and install.
366    pub fn therock_apt_commands(
367        &self,
368        distro: &DistroFacade,
369    ) -> Result<Vec<PackageCommand>, String> {
370        let dir = Self::therock_distro_dir(distro).ok_or_else(|| {
371            format!(
372                "ROCm {} has no apt repo for {} {} (supported: Ubuntu 22.04/24.04/26.04, Debian 12/13)",
373                self.config.channel.version(),
374                distro.id(),
375                distro.version()
376            )
377        })?;
378        let sh = |script: String| PackageCommand {
379            program: "sudo".to_string(),
380            args: vec!["sh".to_string(), "-c".to_string(), script],
381        };
382        // The key is fetched over TLS, then its fingerprint is checked against
383        // the pinned AMD ROCm signing key before it is trusted by apt.
384        let key_script = format!(
385            "set -e; mkdir -p /etc/apt/keyrings; \
386             wget -qO /tmp/rocm-multi-arch.gpg {THEROCK_GPG_URL}; \
387             gpg --show-keys --with-colons /tmp/rocm-multi-arch.gpg | grep -q '^fpr:::::::::{THEROCK_GPG_FINGERPRINT}:' \
388             || {{ echo 'ROCm repo key fingerprint mismatch' >&2; rm -f /tmp/rocm-multi-arch.gpg; exit 1; }}; \
389             mv /tmp/rocm-multi-arch.gpg /etc/apt/keyrings/rocm-multi-arch.gpg"
390        );
391        Ok(vec![
392            sh(key_script),
393            sh(format!(
394                "echo 'deb [arch=amd64 signed-by=/etc/apt/keyrings/rocm-multi-arch.gpg] {THEROCK_REPO_ROOT}/{dir} stable main' > /etc/apt/sources.list.d/rocm-multi-arch.list"
395            )),
396            PackageCommand {
397                program: "sudo".to_string(),
398                args: vec!["apt-get".to_string(), "update".to_string()],
399            },
400            PackageCommand {
401                program: "sudo".to_string(),
402                args: vec![
403                    "apt-get".to_string(),
404                    "install".to_string(),
405                    "-y".to_string(),
406                    format!("{}={}", self.therock_package(), self.therock_apt_version_glob()),
407                ],
408            },
409        ])
410    }
411
412    /// Commands that configure the TheRock rpm repo and install (dnf / zypper).
413    pub fn therock_rpm_commands(
414        &self,
415        distro: &DistroFacade,
416    ) -> Result<Vec<PackageCommand>, String> {
417        let dir = Self::therock_distro_dir(distro).ok_or_else(|| {
418            format!(
419                "ROCm {} has no rpm repo for {} {} (supported: RHEL 8/9/10, SLES 15/16)",
420                self.config.channel.version(),
421                distro.id(),
422                distro.version()
423            )
424        })?;
425        let url = format!("{THEROCK_REPO_ROOT}/{dir}/x86_64");
426        let pkg = format!(
427            "{}-{}",
428            self.therock_package(),
429            self.config.channel.version()
430        );
431        let sudo = |args: Vec<&str>| PackageCommand {
432            program: "sudo".to_string(),
433            args: args.into_iter().map(String::from).collect(),
434        };
435        let import_key = PackageCommand {
436            program: "sh".to_string(),
437            args: vec![
438                "-c".to_string(),
439                format!(
440                    "set -e; wget -qO /tmp/rocm-multi-arch.gpg {THEROCK_GPG_URL}; \
441                     gpg --show-keys --with-colons /tmp/rocm-multi-arch.gpg | grep -q '^fpr:::::::::{THEROCK_GPG_FINGERPRINT}:' \
442                     || {{ echo 'ROCm GPG key fingerprint mismatch' >&2; exit 1; }}; \
443                     sudo rpm --import /tmp/rocm-multi-arch.gpg"
444                ),
445            ],
446        };
447        Ok(if distro.uses_zypper() {
448            vec![
449                import_key,
450                sudo(vec![
451                    "zypper",
452                    "--non-interactive",
453                    "addrepo",
454                    "--refresh",
455                    &url,
456                    "rocm-multi-arch",
457                ]),
458                sudo(vec![
459                    "zypper",
460                    "--non-interactive",
461                    "refresh",
462                ]),
463                sudo(vec!["zypper", "--non-interactive", "install", &pkg]),
464            ]
465        } else {
466            vec![
467                import_key,
468                sudo(vec!["dnf", "config-manager", "--add-repo", &url]),
469                sudo(vec!["dnf", "install", "-y", &pkg]),
470            ]
471        })
472    }
473
474    /// pip requirement for the ROCm SDK on this channel
475    /// (`rocm[libraries,device-all]==7.14.1`).
476    pub fn pip_requirement(&self) -> String {
477        format!(
478            "rocm[libraries,device-all]=={}",
479            self.config.channel.version()
480        )
481    }
482
483    /// pip index URL for this channel (`None` for classic Legacy).
484    pub fn pip_index_url(&self) -> Option<&'static str> {
485        match self.config.channel {
486            RocmChannel::Legacy => None,
487            RocmChannel::Stable | RocmChannel::Latest => Some(THEROCK_PIP_INDEX),
488            RocmChannel::Preview => Some(PREVIEW_PIP_INDEX),
489        }
490    }
491
492    /// TheRock GPU family for tarball names (`gfx1100` -> `gfx110X-all`).
493    pub fn therock_family(gfx: &str) -> Option<String> {
494        let g = gfx.trim().to_ascii_lowercase();
495        let fam = match g.as_str() {
496            s if s.starts_with("gfx101") => "gfx101X-dgpu",
497            s if s.starts_with("gfx103") => "gfx103X-all",
498            s if s.starts_with("gfx110") => "gfx110X-all",
499            s if s.starts_with("gfx115") => "gfx115X-all",
500            s if s.starts_with("gfx120") => "gfx120X-all",
501            "gfx908" => "gfx908",
502            "gfx90a" => "gfx90a",
503            "gfx942" | "gfx941" | "gfx940" => "gfx94X-dcgpu",
504            "gfx950" => "gfx950-dcgpu",
505            _ => return None,
506        };
507        Some(fam.to_string())
508    }
509
510    /// TheRock tarball URL for the configured GPU and channel.
511    pub fn tarball_url(&self) -> Result<String, String> {
512        let family = Self::therock_family(&self.config.gfx_arch).ok_or_else(|| {
513            format!(
514                "no TheRock tarball family known for GPU architecture '{}'",
515                self.config.gfx_arch
516            )
517        })?;
518        Ok(format!(
519            "{THEROCK_TARBALL_ROOT}/therock-dist-linux-{family}-{}.tar.gz",
520            self.config.channel.version()
521        ))
522    }
523
524    /// Runfile installer URL (`rocm-installer-7.14.0-7.run`).
525    ///
526    /// The trailing number is the runfile build id; it defaults to
527    /// [`RUNFILE_BUILD_DEFAULT`] and can be overridden with `ROCM_RUNFILE_BUILD`.
528    pub fn runfile_url(&self) -> String {
529        let build = std::env::var("ROCM_RUNFILE_BUILD")
530            .ok()
531            .map(|b| b.trim().to_string())
532            .filter(|b| !b.is_empty() && b.chars().all(|c| c.is_ascii_digit()))
533            .unwrap_or_else(|| RUNFILE_BUILD_DEFAULT.to_string());
534        let v = self.config.channel.version();
535        let mm = self.config.channel.major_minor();
536        format!("{RUNFILE_ROOT}/rocm-rel-{mm}/rocm-installer-{v}-{build}.run")
537    }
538
539    // -----------------------------------------------------------------------
540    // Repository URL construction (VAL-INSTALL-002)
541    // -----------------------------------------------------------------------
542
543    /// Get the amdgpu/ROCm repo base URL for the given distro and channel.
544    ///
545    /// For apt-based distros, the URL is:
546    /// `https://repo.radeon.com/amdgpu/{major_minor}/ubuntu/{codename}`
547    ///
548    /// For RHEL-based distros:
549    /// `https://repo.radeon.com/amdgpu/{major_minor}/rhel/{rhel_version}`
550    ///
551    /// For SUSE:
552    /// `https://repo.radeon.com/amdgpu/{major_minor}/sle/{sles_version}`
553    pub fn repo_base_url(&self, distro: &DistroFacade) -> String {
554        let major_minor = self.config.channel.major_minor();
555        if distro.uses_apt() {
556            let codename = if distro.codename().is_empty() {
557                "noble"
558            } else {
559                distro.codename()
560            };
561            format!("https://repo.radeon.com/amdgpu/{major_minor}/ubuntu/{codename}")
562        } else if distro.uses_dnf() || distro.uses_yum() {
563            let rhel_version = Self::rhel_version(distro);
564            format!("https://repo.radeon.com/amdgpu/{major_minor}/rhel/{rhel_version}")
565        } else if distro.uses_zypper() {
566            format!("https://repo.radeon.com/amdgpu/{major_minor}/sle/15.7")
567        } else {
568            // Fallback for Arch and others
569            format!("https://repo.radeon.com/amdgpu/{major_minor}/ubuntu/noble")
570        }
571    }
572
573    /// Get the ROCm core repo URL for apt-based distros.
574    pub fn rocm_core_repo_url(&self, _distro: &DistroFacade) -> String {
575        let major_minor = self.config.channel.major_minor();
576        format!("https://repo.radeon.com/rocm/apt/{major_minor}")
577    }
578
579    /// Get the GPG key URL.
580    pub fn gpg_key_url(&self) -> &'static str {
581        "https://repo.radeon.com/rocm/rocm.gpg.key"
582    }
583
584    /// Derive the RHEL major version from distro info.
585    fn rhel_version(distro: &DistroFacade) -> &'static str {
586        let v = distro.version();
587        if v.starts_with("10") {
588            "10"
589        } else if v.starts_with("9") {
590            "9"
591        } else if v.starts_with("8") {
592            "8"
593        } else {
594            "9"
595        }
596    }
597
598    // -----------------------------------------------------------------------
599    // Package command construction (VAL-INSTALL-001)
600    // -----------------------------------------------------------------------
601
602    /// Construct the amdgpu-install package download URL for apt-based distros.
603    pub fn amdgpu_install_deb_url(&self) -> String {
604        let release_dir = self.classic_release();
605        let pkg_ver = self.classic_pkg_version();
606        format!(
607            "https://repo.radeon.com/amdgpu-install/{release_dir}/ubuntu/noble/amdgpu-install_{pkg_ver}_all.deb"
608        )
609    }
610
611    /// Construct the amdgpu-install RPM download URL.
612    pub fn amdgpu_install_rpm_url(&self, rhel_version: &str) -> String {
613        let release_dir = self.classic_release();
614        let pkg_ver = self.classic_pkg_version();
615        format!(
616            "https://repo.radeon.com/amdgpu-install/{release_dir}/rhel/{rhel_version}/amdgpu-install-{pkg_ver}.el{rhel_version}.noarch.rpm"
617        )
618    }
619
620    /// Construct the amdgpu-install SLES RPM download URL.
621    pub fn amdgpu_install_sles_rpm_url(&self) -> String {
622        let release_dir = self.classic_release();
623        let pkg_ver = self.classic_pkg_version();
624        format!(
625            "https://repo.radeon.com/amdgpu-install/{release_dir}/sle/15.7/amdgpu-install-{pkg_ver}.noarch.rpm"
626        )
627    }
628
629    /// Construct the apt-get install command for amdgpu-install.
630    pub fn apt_install_commands(&self) -> Vec<PackageCommand> {
631        debug_assert!(
632            !self.config.channel.is_therock(),
633            "TheRock channels use therock_*_commands"
634        );
635        let deb_url = self.amdgpu_install_deb_url();
636        vec![
637            // Download the .deb
638            PackageCommand {
639                program: "wget".to_string(),
640                args: vec!["-q".to_string(), deb_url.clone()],
641            },
642            // Install it
643            PackageCommand {
644                program: "sudo".to_string(),
645                args: vec![
646                    "dpkg".to_string(),
647                    "-i".to_string(),
648                    format!("amdgpu-install_{}_all.deb", self.classic_pkg_version()),
649                ],
650            },
651            // Update and install ROCm packages
652            PackageCommand {
653                program: "sudo".to_string(),
654                args: vec!["apt-get".to_string(), "update".to_string()],
655            },
656            // Install ROCm metapackage based on install type
657            self.apt_rocm_metapackage_command(),
658        ]
659    }
660
661    /// Get the apt metapackage install command for the selected install type.
662    fn apt_rocm_metapackage_command(&self) -> PackageCommand {
663        let metapkg = match self.config.install_type {
664            RocmInstallType::Standard => "rocm-libs",
665            RocmInstallType::Minimal => "rocm-core",
666            RocmInstallType::Full => "rocm-dev",
667            RocmInstallType::Custom => "rocm-libs",
668        };
669        PackageCommand {
670            program: "sudo".to_string(),
671            args: vec![
672                "apt-get".to_string(),
673                "install".to_string(),
674                "-y".to_string(),
675                metapkg.to_string(),
676            ],
677        }
678    }
679
680    /// Construct the dnf install commands for ROCm.
681    pub fn dnf_install_commands(&self, rhel_version: &str) -> Vec<PackageCommand> {
682        debug_assert!(
683            !self.config.channel.is_therock(),
684            "TheRock channels use therock_*_commands"
685        );
686        let rpm_url = self.amdgpu_install_rpm_url(rhel_version);
687        vec![
688            PackageCommand {
689                program: "sudo".to_string(),
690                args: vec![
691                    "dnf".to_string(),
692                    "install".to_string(),
693                    "-y".to_string(),
694                    rpm_url,
695                ],
696            },
697            self.dnf_rocm_metapackage_command(),
698        ]
699    }
700
701    /// Get the dnf metapackage install command.
702    fn dnf_rocm_metapackage_command(&self) -> PackageCommand {
703        let metapkg = match self.config.install_type {
704            RocmInstallType::Standard => "rocm-libs",
705            RocmInstallType::Minimal => "rocm-core",
706            RocmInstallType::Full => "rocm-dev",
707            RocmInstallType::Custom => "rocm-libs",
708        };
709        PackageCommand {
710            program: "sudo".to_string(),
711            args: vec![
712                "dnf".to_string(),
713                "install".to_string(),
714                "-y".to_string(),
715                metapkg.to_string(),
716            ],
717        }
718    }
719
720    /// Construct the pacman install commands for ROCm on Arch.
721    ///
722    /// Arch uses AUR packages (yay/paru) for ROCm.
723    /// Build the pacman/AUR (`yay`) install command for ROCm.
724    ///
725    /// `yay` MUST run as the user — makepkg refuses root and `yay` warns
726    /// "Avoid running yay as root/sudo". This therefore returns a SINGLE
727    /// user-space command, never wrapped in `sudo`. (A bare `sudo yay` with no
728    /// operation defaults to `yay -Syu`, i.e. a full system upgrade, which is
729    /// what previously aborted ROCm installs.) The caller feeds yay's internal
730    /// `sudo pacman` non-interactively — e.g. `SUDO_ASKPASS` +
731    /// `--sudoflags=-A`, as the installer's Arch path does.
732    pub fn pacman_install_commands(&self, aur_helper: &str) -> Vec<PackageCommand> {
733        let packages = self.pacman_rocm_packages();
734        let mut args = vec![
735            "-S".to_string(),
736            "--needed".to_string(),
737            "--noconfirm".to_string(),
738        ];
739        args.extend(packages);
740
741        vec![PackageCommand {
742            program: aur_helper.to_string(),
743            args,
744        }]
745    }
746
747    /// Get the list of ROCm AUR packages for pacman-based installs.
748    pub fn pacman_rocm_packages(&self) -> Vec<String> {
749        let mut pkgs = vec![
750            "rocm-hip-sdk".to_string(),
751            "rocminfo".to_string(),
752            "rocm-smi-lib".to_string(),
753            "hipblas".to_string(),
754            "rocblas".to_string(),
755            "hipsparselt".to_string(),
756        ];
757        // Add extra packages for Stable/Latest channels
758        if self.config.channel != RocmChannel::Legacy {
759            pkgs.push("rocm-opencl-sdk".to_string());
760            pkgs.push("rccl".to_string());
761            // rocwmma: RDNA3+ WMMA flash-attention backend (used by Rusty Llama's
762            // fattn-wmma-f16). Optional at build time (the fork gates on it), but
763            // install it so the WMMA FA path is available out of the box.
764            pkgs.push("rocwmma".to_string());
765        }
766        pkgs
767    }
768
769    /// Construct the zypper install commands for ROCm on SUSE.
770    pub fn zypper_install_commands(&self) -> Vec<PackageCommand> {
771        debug_assert!(
772            !self.config.channel.is_therock(),
773            "TheRock channels use therock_*_commands"
774        );
775        let rpm_url = self.amdgpu_install_sles_rpm_url();
776        vec![
777            PackageCommand {
778                program: "sudo".to_string(),
779                args: vec![
780                    "zypper".to_string(),
781                    "--non-interactive".to_string(),
782                    "install".to_string(),
783                    rpm_url,
784                ],
785            },
786            PackageCommand {
787                program: "sudo".to_string(),
788                args: vec![
789                    "zypper".to_string(),
790                    "--non-interactive".to_string(),
791                    "install".to_string(),
792                    "-y".to_string(),
793                    "rocm-libs".to_string(),
794                ],
795            },
796        ]
797    }
798
799    /// Get the repository configuration for apt-based distros.
800    pub fn apt_repo_config(&self, distro: &DistroFacade) -> RepoConfig {
801        let codename = if distro.codename().is_empty() {
802            "noble"
803        } else {
804            distro.codename()
805        };
806        let major_minor = self.config.channel.major_minor();
807        RepoConfig {
808            repo_type: "apt".to_string(),
809            url: format!("deb [signed-by=/etc/apt/keyrings/rocm.gpg] https://repo.radeon.com/rocm/apt/{major_minor} {codename} main"),
810            gpg_key_url: "https://repo.radeon.com/rocm/rocm.gpg.key".to_string(),
811            file_path: "/etc/apt/sources.list.d/rocm.list".to_string(),
812        }
813    }
814
815    /// Get the amdgpu repo configuration for apt-based distros.
816    pub fn amdgpu_apt_repo_config(&self, distro: &DistroFacade) -> RepoConfig {
817        let codename = if distro.codename().is_empty() {
818            "noble"
819        } else {
820            distro.codename()
821        };
822        let major_minor = self.config.channel.major_minor();
823        RepoConfig {
824            repo_type: "apt".to_string(),
825            url: format!("deb [signed-by=/etc/apt/keyrings/rocm.gpg] https://repo.radeon.com/amdgpu/{major_minor}/ubuntu/{codename} {codename} main"),
826            gpg_key_url: "https://repo.radeon.com/rocm/rocm.gpg.key".to_string(),
827            file_path: "/etc/apt/sources.list.d/amdgpu.list".to_string(),
828        }
829    }
830
831    /// Get the ROCm preference pin configuration for apt.
832    pub fn apt_pin_config(&self) -> (String, String) {
833        (
834            "Package: *\nPin: release o=repo.radeon.com\nPin-Priority: 600\n".to_string(),
835            "/etc/apt/preferences.d/rocm-pin-600".to_string(),
836        )
837    }
838
839    /// Build a command to check if a single package is installed on Arch/pacman.
840    ///
841    /// Returns a `PackageCommand` that runs `pacman -Qi <package>`.
842    /// Exit code 0 means installed, non-zero means not installed.
843    pub fn build_pacman_check_installed_command(package: &str) -> PackageCommand {
844        PackageCommand {
845            program: "pacman".to_string(),
846            args: vec!["-Qi".to_string(), package.to_string()],
847        }
848    }
849
850    /// Filter out packages that are already installed on Arch/pacman systems.
851    ///
852    /// For each package in `packages`, runs `pacman -Qi <pkg>` to check if
853    /// it's installed. Returns only the packages that are NOT yet installed.
854    /// In dry-run mode, returns all packages (no checks performed).
855    pub fn filter_already_installed_pacman(packages: &[String]) -> Vec<String> {
856        use std::process::Command;
857
858        packages
859            .iter()
860            .filter(|pkg| {
861                let status = Command::new("pacman").args(["-Qi", pkg.as_str()]).output();
862                match status {
863                    Ok(output) => !output.status.success(),
864                    Err(_) => true, // If pacman fails to run, assume not installed
865                }
866            })
867            .cloned()
868            .collect()
869    }
870
871    /// Check if command output indicates a package is already up to date.
872    ///
873    /// yay/pacman output patterns that should be treated as success:
874    /// - `warning: <pkg> is up to date -- skipping`
875    /// - `<pkg> is up to date -- skipping`
876    /// - `there is nothing to do`
877    /// - `already installed`
878    /// - `.rc files restored` (post-install message, not an error)
879    pub fn is_up_to_date_output(output: &str) -> bool {
880        let lower = output.to_lowercase();
881        // yay/pacman "up to date" patterns
882        lower.contains("up to date -- skipping")
883            || lower.contains("up to date, skipping")
884            || lower.contains("up to date -- reinstalling")
885            || lower.contains("there is nothing to do")
886            || lower.contains("already installed")
887            // pacman -S on already-installed package
888            || lower.contains("warning:") && lower.contains("up to date")
889    }
890}
891
892// ===========================================================================
893// Tests
894// ===========================================================================
895
896#[cfg(test)]
897mod tests {
898    use super::*;
899    use crate::platform::detection::{DistroFamily, DistroInfo, PackageManager};
900
901    fn ubuntu_distro() -> DistroFacade {
902        DistroFacade::from_info(DistroInfo {
903            id: "ubuntu".to_string(),
904            name: "Ubuntu 24.04 LTS".to_string(),
905            version: "24.04".to_string(),
906            codename: "noble".to_string(),
907            family: DistroFamily::Debian,
908            pkg_manager: PackageManager::Apt,
909            id_like: "debian".to_string(),
910        })
911    }
912
913    fn fedora_distro() -> DistroFacade {
914        DistroFacade::from_info(DistroInfo {
915            id: "fedora".to_string(),
916            name: "Fedora Linux 41".to_string(),
917            version: "41".to_string(),
918            codename: String::new(),
919            family: DistroFamily::Rhel,
920            pkg_manager: PackageManager::Dnf,
921            id_like: "fedora".to_string(),
922        })
923    }
924
925    fn rhel9_distro() -> DistroFacade {
926        DistroFacade::from_info(DistroInfo {
927            id: "rhel".to_string(),
928            name: "Red Hat Enterprise Linux 9".to_string(),
929            version: "9.3".to_string(),
930            codename: String::new(),
931            family: DistroFamily::Rhel,
932            pkg_manager: PackageManager::Dnf,
933            id_like: "rhel".to_string(),
934        })
935    }
936
937    fn suse_distro() -> DistroFacade {
938        DistroFacade::from_info(DistroInfo {
939            id: "opensuse-leap".to_string(),
940            name: "openSUSE Leap 15.7".to_string(),
941            version: "15.7".to_string(),
942            codename: String::new(),
943            family: DistroFamily::Suse,
944            pkg_manager: PackageManager::Zypper,
945            id_like: "suse".to_string(),
946        })
947    }
948
949    // --- VAL-INSTALL-002: ROCm channel selection correct version pins ---
950
951    #[test]
952    fn test_channel_versions() {
953        assert_eq!(RocmChannel::Legacy.version(), "7.2.4");
954        assert_eq!(RocmChannel::Legacy.pkg_version(), "7.2.4.70204-1");
955        assert_eq!(RocmChannel::Legacy.major_minor(), "7.2");
956        assert_eq!(RocmChannel::Stable.version(), "7.14.0");
957        assert_eq!(RocmChannel::Latest.version(), "7.14.1");
958        assert_eq!(RocmChannel::Stable.major_minor(), "7.14");
959        assert_eq!(RocmChannel::Preview.version(), "10.1.0");
960        assert_eq!(RocmChannel::Preview.major_minor(), "10.1");
961        assert!(!RocmChannel::Legacy.is_therock());
962        assert!(RocmChannel::Preview.is_therock());
963    }
964
965    #[test]
966    fn test_channel_from_choice() {
967        assert_eq!(RocmChannel::from_choice(1), Some(RocmChannel::Legacy));
968        assert_eq!(RocmChannel::from_choice(2), Some(RocmChannel::Stable));
969        assert_eq!(RocmChannel::from_choice(3), Some(RocmChannel::Latest));
970        assert_eq!(RocmChannel::from_choice(4), Some(RocmChannel::Preview));
971        assert_eq!(RocmChannel::from_choice(0), None);
972        assert_eq!(RocmChannel::from_choice(5), None);
973    }
974
975    #[test]
976    fn test_channel_parse_and_for_version() {
977        assert_eq!(RocmChannel::parse("Preview"), Some(RocmChannel::Preview));
978        assert_eq!(RocmChannel::parse("bogus"), None);
979        assert_eq!(RocmChannel::for_version("10.1.0"), RocmChannel::Preview);
980        assert_eq!(RocmChannel::for_version("7.14.1"), RocmChannel::Latest);
981        assert_eq!(RocmChannel::for_version("7.14.0"), RocmChannel::Stable);
982        assert_eq!(RocmChannel::for_version("7.2.4"), RocmChannel::Legacy);
983        assert_eq!(RocmChannel::for_version("6.4.3"), RocmChannel::Legacy);
984    }
985
986    #[test]
987    fn test_legacy_fallback_723() {
988        let i = RocmInstaller::new(RocmConfig {
989            channel: RocmChannel::Legacy,
990            legacy_fallback: true,
991            ..Default::default()
992        });
993        assert_eq!(i.effective_version(), "7.2.3");
994        assert!(i
995            .amdgpu_install_deb_url()
996            .contains("7.2.3/ubuntu/noble/amdgpu-install_7.2.3.70203-1_all.deb"));
997    }
998
999    #[test]
1000    fn test_therock_urls_and_package() {
1001        let i = RocmInstaller::new(RocmConfig {
1002            channel: RocmChannel::Stable,
1003            gfx_arch: "gfx1100".into(),
1004            ..Default::default()
1005        });
1006        assert_eq!(i.therock_package(), "amdrocm7.14-gfx1100");
1007        assert_eq!(i.pip_requirement(), "rocm[libraries,device-all]==7.14.0");
1008        assert_eq!(
1009            i.pip_index_url(),
1010            Some("https://repo.amd.com/rocm/whl-multi-arch/")
1011        );
1012        assert_eq!(
1013            i.tarball_url().unwrap(),
1014            "https://repo.amd.com/rocm/tarball-multi-arch/therock-dist-linux-gfx110X-all-7.14.0.tar.gz"
1015        );
1016        assert_eq!(
1017            i.runfile_url(),
1018            "https://repo.radeon.com/installer/rocm-runfile-installer/rocm-rel-7.14/rocm-installer-7.14.0-7.run"
1019        );
1020    }
1021
1022    #[test]
1023    fn test_therock_apt_commands_latest() {
1024        let i = RocmInstaller::new(RocmConfig {
1025            channel: RocmChannel::Latest,
1026            ..Default::default()
1027        });
1028        let cmds = i.therock_apt_commands(&ubuntu_distro()).unwrap();
1029        let all: String = cmds
1030            .iter()
1031            .map(|c| c.to_command_string())
1032            .collect::<Vec<_>>()
1033            .join("\n");
1034        assert!(all.contains("packages-multi-arch/gpg/rocm.gpg"));
1035        assert!(all.contains("packages-multi-arch/ubuntu2404 stable main"));
1036        assert!(all.contains("amdrocm7.14-gfx1100=7.14.1-*"));
1037    }
1038
1039    #[test]
1040    fn test_therock_rpm_and_distro_dir() {
1041        let i = RocmInstaller::new(RocmConfig {
1042            channel: RocmChannel::Latest,
1043            ..Default::default()
1044        });
1045        assert_eq!(
1046            RocmInstaller::therock_distro_dir(&ubuntu_distro()).as_deref(),
1047            Some("ubuntu2404")
1048        );
1049        assert_eq!(
1050            RocmInstaller::therock_distro_dir(&rhel9_distro()).as_deref(),
1051            Some("rhel9")
1052        );
1053        let cmds = i.therock_rpm_commands(&rhel9_distro()).unwrap();
1054        assert!(cmds
1055            .iter()
1056            .any(|c| c.to_command_string().contains("amdrocm7.14-gfx1100-7.14.1")));
1057    }
1058
1059    #[test]
1060    fn test_therock_unknown_gpu_and_distro_error() {
1061        let i = RocmInstaller::new(RocmConfig {
1062            channel: RocmChannel::Latest,
1063            gfx_arch: "gfx9999".into(),
1064            ..Default::default()
1065        });
1066        assert!(i.tarball_url().is_err());
1067        assert_eq!(RocmInstaller::therock_family("gfx9999"), None);
1068    }
1069
1070    #[test]
1071    fn test_therock_apt_pins_gpg_fingerprint() {
1072        let i = RocmInstaller::new(RocmConfig {
1073            channel: RocmChannel::Latest,
1074            ..Default::default()
1075        });
1076        let all: String = i
1077            .therock_apt_commands(&ubuntu_distro())
1078            .unwrap()
1079            .iter()
1080            .map(|c| c.to_command_string())
1081            .collect();
1082        assert!(all.contains(THEROCK_GPG_FINGERPRINT));
1083    }
1084
1085    #[test]
1086    fn test_pkg_version_classification_matches_for_version() {
1087        for v in ["8.0.0", "9.1.0", "7.2.4", "7.13.0"] {
1088            assert!(!RocmChannel::for_version(v).is_therock(), "{v}");
1089        }
1090        for v in ["7.14.0", "7.14.1", "10.1.0"] {
1091            assert!(RocmChannel::for_version(v).is_therock(), "{v}");
1092        }
1093    }
1094
1095    #[test]
1096    fn test_preview_channel_consistency() {
1097        let ch = RocmChannel::from_choice(4).unwrap();
1098        assert_eq!(ch, RocmChannel::Preview);
1099        assert_eq!(ch.version(), "10.1.0");
1100        assert_eq!(ch.major_minor(), "10.1");
1101        let i = RocmInstaller::new(RocmConfig {
1102            channel: ch,
1103            ..Default::default()
1104        });
1105        assert_eq!(i.pip_requirement(), "rocm[libraries,device-all]==10.1.0");
1106    }
1107
1108    #[test]
1109    fn test_preview_pip_index() {
1110        let i = RocmInstaller::new(RocmConfig {
1111            channel: RocmChannel::Preview,
1112            ..Default::default()
1113        });
1114        assert_eq!(
1115            i.pip_index_url(),
1116            Some("https://stable.repo.amd.com/rocm/whl-next/")
1117        );
1118        assert_eq!(i.pip_requirement(), "rocm[libraries,device-all]==10.1.0");
1119    }
1120
1121    #[test]
1122    fn test_pkg_version_from_manifest_latest() {
1123        let manifest = crate::core::manifest::Manifest {
1124            schema_version: 2,
1125            sequence: 1,
1126            generated_at: "2026-05-31T00:00:00Z".to_string(),
1127            expires_at: None,
1128            min_runtime_version: String::new(),
1129            components: vec![crate::core::manifest::ManifestComponent {
1130                id: "rocm".to_string(),
1131                version: "7.2.4".to_string(),
1132                script: "scripts/install_rocm.sh".to_string(),
1133                category: crate::core::Category::Foundation,
1134                validation_tier: crate::core::ValidationTier::Validated,
1135                min_rocm_version: String::new(),
1136                dependencies: vec![],
1137                compatible_channels: vec!["latest".to_string()],
1138                exclusive_group: String::new(),
1139            }],
1140            signature: None,
1141        };
1142
1143        assert_eq!(
1144            RocmChannel::pkg_version_from_manifest(&manifest),
1145            "7.2.4.70204-1"
1146        );
1147    }
1148
1149    #[test]
1150    fn test_pkg_version_from_manifest_legacy() {
1151        let manifest = crate::core::manifest::Manifest {
1152            schema_version: 2,
1153            sequence: 1,
1154            generated_at: "2026-05-31T00:00:00Z".to_string(),
1155            expires_at: None,
1156            min_runtime_version: String::new(),
1157            components: vec![crate::core::manifest::ManifestComponent {
1158                id: "rocm".to_string(),
1159                version: "6.4.3".to_string(),
1160                script: "scripts/install_rocm.sh".to_string(),
1161                category: crate::core::Category::Foundation,
1162                validation_tier: crate::core::ValidationTier::Validated,
1163                min_rocm_version: String::new(),
1164                dependencies: vec![],
1165                compatible_channels: vec!["legacy".to_string()],
1166                exclusive_group: String::new(),
1167            }],
1168            signature: None,
1169        };
1170
1171        assert_eq!(
1172            RocmChannel::pkg_version_from_manifest(&manifest),
1173            "6.4.60403-1"
1174        );
1175    }
1176
1177    // --- VAL-INSTALL-001: ROCm installer correct package commands ---
1178
1179    #[test]
1180    fn test_apt_install_commands_latest() {
1181        let installer = RocmInstaller::new(RocmConfig {
1182            channel: RocmChannel::Legacy,
1183            install_type: RocmInstallType::Standard,
1184            ..Default::default()
1185        });
1186        let cmds = installer.apt_install_commands();
1187        assert_eq!(cmds.len(), 4);
1188
1189        // wget download
1190        assert_eq!(cmds[0].program, "wget");
1191        assert!(cmds[0]
1192            .args
1193            .iter()
1194            .any(|a| a.contains("7.2.4/ubuntu/noble")));
1195        assert!(cmds[0].args.iter().any(|a| a.contains("7.2.4.70204-1")));
1196        assert_eq!(cmds[1].program, "sudo");
1197        assert!(cmds[1].args.contains(&"dpkg".to_string()));
1198
1199        // apt-get update
1200        assert_eq!(cmds[2].program, "sudo");
1201        assert!(cmds[2].args.contains(&"update".to_string()));
1202
1203        // rocm-libs metapackage
1204        assert_eq!(cmds[3].program, "sudo");
1205        assert!(cmds[3].args.contains(&"rocm-libs".to_string()));
1206    }
1207
1208    #[test]
1209    fn test_apt_install_commands_full() {
1210        let installer = RocmInstaller::new(RocmConfig {
1211            channel: RocmChannel::Legacy,
1212            install_type: RocmInstallType::Full,
1213            ..Default::default()
1214        });
1215        let cmds = installer.apt_install_commands();
1216        let meta_cmd = &cmds[3];
1217        assert!(meta_cmd.args.contains(&"rocm-dev".to_string()));
1218    }
1219
1220    #[test]
1221    fn test_dnf_install_commands() {
1222        let installer = RocmInstaller::new(RocmConfig {
1223            channel: RocmChannel::Legacy,
1224            install_type: RocmInstallType::Standard,
1225            ..Default::default()
1226        });
1227        let cmds = installer.dnf_install_commands("9");
1228        assert_eq!(cmds.len(), 2);
1229
1230        // dnf install RPM
1231        assert_eq!(cmds[0].program, "sudo");
1232        assert!(cmds[0].args.iter().any(|a| a.contains("7.2.4/rhel/9")));
1233        assert!(cmds[0].args.iter().any(|a| a.contains("7.2.4.70204-1")));
1234        assert!(cmds[0].args.iter().any(|a| a.contains("el9")));
1235
1236        // rocm-libs metapackage
1237        assert_eq!(cmds[1].program, "sudo");
1238        assert!(cmds[1].args.contains(&"rocm-libs".to_string()));
1239    }
1240
1241    #[test]
1242    fn test_pacman_install_commands() {
1243        let installer = RocmInstaller::new(RocmConfig {
1244            channel: RocmChannel::Latest,
1245            install_type: RocmInstallType::Standard,
1246            ..Default::default()
1247        });
1248        let cmds = installer.pacman_install_commands("yay");
1249        // Single user-space `yay` command — never a bare `sudo yay` (which
1250        // defaults to `yay -Syu`, a full system upgrade as root). Regression
1251        // guard for the malformed two-command [sudo yay, yay …] that aborted
1252        // ROCm installs on Arch.
1253        assert_eq!(
1254            cmds.len(),
1255            1,
1256            "pacman install must be a single user-space yay command"
1257        );
1258        assert_eq!(cmds[0].program, "yay");
1259        assert!(
1260            !cmds.iter().any(|c| c.program == "sudo"),
1261            "yay must not run under sudo"
1262        );
1263        assert!(cmds[0].args.contains(&"-S".to_string()));
1264        assert!(cmds[0].args.contains(&"--needed".to_string()));
1265        assert!(cmds[0].args.contains(&"--noconfirm".to_string()));
1266        // Should have packages for Latest channel
1267        let pkgs = installer.pacman_rocm_packages();
1268        assert!(pkgs.contains(&"rocm-hip-sdk".to_string()));
1269        assert!(pkgs.contains(&"rocminfo".to_string()));
1270        assert!(pkgs.contains(&"rocm-opencl-sdk".to_string())); // Latest gets extra
1271        assert!(pkgs.contains(&"rccl".to_string())); // Latest gets extra
1272        assert!(pkgs.contains(&"rocwmma".to_string())); // Latest: WMMA FA backend
1273        for p in &pkgs {
1274            assert!(cmds[0].args.contains(p), "yay args missing package {p}");
1275        }
1276    }
1277
1278    #[test]
1279    fn test_pacman_legacy_no_extras() {
1280        let installer = RocmInstaller::new(RocmConfig {
1281            channel: RocmChannel::Legacy,
1282            install_type: RocmInstallType::Standard,
1283            ..Default::default()
1284        });
1285        let pkgs = installer.pacman_rocm_packages();
1286        assert!(!pkgs.contains(&"rocm-opencl-sdk".to_string()));
1287        assert!(!pkgs.contains(&"rccl".to_string()));
1288        assert!(!pkgs.contains(&"rocwmma".to_string())); // Legacy (RDNA2): no WMMA
1289    }
1290
1291    #[test]
1292    fn test_zypper_install_commands() {
1293        let installer = RocmInstaller::new(RocmConfig {
1294            channel: RocmChannel::Legacy,
1295            install_type: RocmInstallType::Standard,
1296            ..Default::default()
1297        });
1298        let cmds = installer.zypper_install_commands();
1299        assert_eq!(cmds.len(), 2);
1300        assert_eq!(cmds[0].program, "sudo");
1301        assert!(cmds[0].args.iter().any(|a| a.contains("zypper")));
1302        assert!(cmds[0].args.iter().any(|a| a.contains("7.2.4/sle/15.7")));
1303        assert!(cmds[0].args.iter().any(|a| a.contains("7.2.4.70204-1")));
1304    }
1305
1306    // --- Repo URL construction ---
1307
1308    #[test]
1309    fn test_repo_base_url_ubuntu() {
1310        let installer = RocmInstaller::new(RocmConfig {
1311            channel: RocmChannel::Legacy,
1312            ..Default::default()
1313        });
1314        let distro = ubuntu_distro();
1315        let url = installer.repo_base_url(&distro);
1316        assert_eq!(url, "https://repo.radeon.com/amdgpu/7.2/ubuntu/noble");
1317    }
1318
1319    #[test]
1320    fn test_repo_base_url_rhel() {
1321        let installer = RocmInstaller::new(RocmConfig {
1322            channel: RocmChannel::Legacy,
1323            ..Default::default()
1324        });
1325        let distro = rhel9_distro();
1326        let url = installer.repo_base_url(&distro);
1327        assert_eq!(url, "https://repo.radeon.com/amdgpu/7.2/rhel/9");
1328    }
1329
1330    #[test]
1331    fn test_repo_base_url_fedora() {
1332        let installer = RocmInstaller::new(RocmConfig {
1333            channel: RocmChannel::Legacy,
1334            ..Default::default()
1335        });
1336        let distro = fedora_distro();
1337        let url = installer.repo_base_url(&distro);
1338        // Fedora version "41" doesn't match 8/9/10, defaults to 9
1339        assert_eq!(url, "https://repo.radeon.com/amdgpu/7.2/rhel/9");
1340    }
1341
1342    #[test]
1343    fn test_repo_base_url_suse() {
1344        let installer = RocmInstaller::new(RocmConfig {
1345            channel: RocmChannel::Legacy,
1346            ..Default::default()
1347        });
1348        let distro = suse_distro();
1349        let url = installer.repo_base_url(&distro);
1350        assert_eq!(url, "https://repo.radeon.com/amdgpu/7.2/sle/15.7");
1351    }
1352
1353    #[test]
1354    fn test_amdgpu_install_deb_url() {
1355        let installer = RocmInstaller::new(RocmConfig {
1356            channel: RocmChannel::Legacy,
1357            ..Default::default()
1358        });
1359        let url = installer.amdgpu_install_deb_url();
1360        assert!(url.contains("7.2.4/ubuntu/noble"));
1361        assert!(url.contains("7.2.4.70204-1"));
1362        assert!(url.contains("ubuntu/noble"));
1363        assert!(url.ends_with(".deb"));
1364    }
1365
1366    #[test]
1367    fn test_amdgpu_install_rpm_url() {
1368        let installer = RocmInstaller::new(RocmConfig {
1369            channel: RocmChannel::Legacy,
1370            ..Default::default()
1371        });
1372        let url = installer.amdgpu_install_rpm_url("9");
1373        assert!(url.contains("7.2.4/rhel/9"));
1374        assert!(url.contains("7.2.4.70204-1"));
1375        assert!(url.contains("rhel/9"));
1376        assert!(url.ends_with(".noarch.rpm"));
1377    }
1378
1379    #[test]
1380    fn test_apt_repo_config() {
1381        let installer = RocmInstaller::new(RocmConfig {
1382            channel: RocmChannel::Legacy,
1383            ..Default::default()
1384        });
1385        let distro = ubuntu_distro();
1386        let repo = installer.apt_repo_config(&distro);
1387        assert_eq!(repo.file_path, "/etc/apt/sources.list.d/rocm.list");
1388        assert!(repo.url.contains("rocm/apt/7.2"));
1389        assert!(repo.url.contains("noble"));
1390    }
1391
1392    #[test]
1393    fn test_amdgpu_apt_repo_config() {
1394        let installer = RocmInstaller::new(RocmConfig {
1395            channel: RocmChannel::Legacy,
1396            ..Default::default()
1397        });
1398        let distro = ubuntu_distro();
1399        let repo = installer.amdgpu_apt_repo_config(&distro);
1400        assert_eq!(repo.file_path, "/etc/apt/sources.list.d/amdgpu.list");
1401        assert!(repo.url.contains("amdgpu/7.2"));
1402    }
1403
1404    #[test]
1405    fn test_apt_pin_config() {
1406        let installer = RocmInstaller::with_defaults();
1407        let (content, path) = installer.apt_pin_config();
1408        assert_eq!(path, "/etc/apt/preferences.d/rocm-pin-600");
1409        assert!(content.contains("Pin-Priority: 600"));
1410    }
1411
1412    // --- Cross-channel parametric tests ---
1413
1414    #[test]
1415    fn test_all_channels_version_consistency() {
1416        for choice in [1u8, 2, 3] {
1417            let ch = RocmChannel::from_choice(choice).unwrap();
1418            let config = RocmConfig {
1419                channel: ch,
1420                ..Default::default()
1421            };
1422            let installer = RocmInstaller::new(config);
1423
1424            // Verify deb URL contains correct package version
1425            let deb_url = installer.amdgpu_install_deb_url();
1426            assert!(
1427                deb_url.contains(ch.pkg_version()),
1428                "deb URL for {:?} should contain pkg_version {}",
1429                ch,
1430                ch.pkg_version()
1431            );
1432
1433            // Verify repo URL contains major_minor
1434            let distro = ubuntu_distro();
1435            let repo_url = installer.repo_base_url(&distro);
1436            assert!(
1437                repo_url.contains(ch.major_minor()),
1438                "repo URL for {:?} should contain major_minor {}",
1439                ch,
1440                ch.major_minor()
1441            );
1442        }
1443    }
1444
1445    #[test]
1446    fn test_install_type_metapackages() {
1447        for (itype, expected_pkg) in [
1448            (RocmInstallType::Standard, "rocm-libs"),
1449            (RocmInstallType::Minimal, "rocm-core"),
1450            (RocmInstallType::Full, "rocm-dev"),
1451            (RocmInstallType::Custom, "rocm-libs"),
1452        ] {
1453            let installer = RocmInstaller::new(RocmConfig {
1454                channel: RocmChannel::Legacy,
1455                install_type: itype,
1456                ..Default::default()
1457            });
1458            let cmds = installer.apt_install_commands();
1459            let meta_cmd = &cmds[3];
1460            assert!(
1461                meta_cmd.args.contains(&expected_pkg.to_string()),
1462                "Install type {:?} should use metapackage {}",
1463                itype,
1464                expected_pkg
1465            );
1466        }
1467    }
1468
1469    // --- Up-to-date output detection ---
1470
1471    #[test]
1472    fn test_is_up_to_date_yay_warning() {
1473        assert!(RocmInstaller::is_up_to_date_output(
1474            "warning: rocminfo is up to date -- skipping"
1475        ));
1476    }
1477
1478    #[test]
1479    fn test_is_up_to_date_yay_warning_case_insensitive() {
1480        assert!(RocmInstaller::is_up_to_date_output(
1481            "WARNING: rocminfo is UP TO DATE -- skipping"
1482        ));
1483    }
1484
1485    #[test]
1486    fn test_is_up_to_date_nothing_to_do() {
1487        assert!(RocmInstaller::is_up_to_date_output(
1488            " there is nothing to do\n"
1489        ));
1490    }
1491
1492    #[test]
1493    fn test_is_up_to_date_already_installed() {
1494        assert!(RocmInstaller::is_up_to_date_output(
1495            "package already installed"
1496        ));
1497    }
1498
1499    #[test]
1500    fn test_is_up_to_date_normal_output_is_false() {
1501        assert!(!RocmInstaller::is_up_to_date_output(
1502            "resolving dependencies...\nlooking for conflicting packages...\n"
1503        ));
1504    }
1505
1506    #[test]
1507    fn test_is_up_to_date_error_output_is_false() {
1508        assert!(!RocmInstaller::is_up_to_date_output(
1509            "error: failed to prepare transaction\nerror: target not found: rocminfo"
1510        ));
1511    }
1512
1513    #[test]
1514    fn test_is_up_to_date_empty_output_is_false() {
1515        assert!(!RocmInstaller::is_up_to_date_output(""));
1516    }
1517
1518    #[test]
1519    fn test_is_up_to_date_pacman_skipping() {
1520        assert!(RocmInstaller::is_up_to_date_output(
1521            "warning: rocm-hip-sdk is up to date -- skipping"
1522        ));
1523    }
1524
1525    // --- Pacman check-installed command construction ---
1526
1527    #[test]
1528    fn test_build_pacman_check_installed_command() {
1529        let cmd = RocmInstaller::build_pacman_check_installed_command("rocminfo");
1530        assert_eq!(cmd.program, "pacman");
1531        assert_eq!(cmd.args, vec!["-Qi", "rocminfo"]);
1532    }
1533
1534    #[test]
1535    fn test_build_pacman_check_installed_command_complex_pkg() {
1536        let cmd = RocmInstaller::build_pacman_check_installed_command("rocm-hip-sdk");
1537        assert_eq!(cmd.program, "pacman");
1538        assert!(cmd.args.contains(&"rocm-hip-sdk".to_string()));
1539    }
1540}