1use crate::installers::common::DistroFacade;
13use std::fmt;
14
15pub const LEGACY_FALLBACK_VERSION: &str = "7.2.3";
21pub const THEROCK_REPO_ROOT: &str = "https://repo.amd.com/rocm/packages-multi-arch";
23pub const THEROCK_GPG_URL: &str = "https://repo.amd.com/rocm/packages-multi-arch/gpg/rocm.gpg";
25pub const THEROCK_GPG_FINGERPRINT: &str = "D0F004A0025A1145C7807FCD0701EAC4D5E02107";
27pub const RUNFILE_BUILD_DEFAULT: &str = "7";
29pub const THEROCK_PIP_INDEX: &str = "https://repo.amd.com/rocm/whl-multi-arch/";
31pub const PREVIEW_PIP_INDEX: &str = "https://stable.repo.amd.com/rocm/whl-next/";
33pub const THEROCK_TARBALL_ROOT: &str = "https://repo.amd.com/rocm/tarball-multi-arch";
35pub const RUNFILE_ROOT: &str = "https://repo.radeon.com/installer/rocm-runfile-installer";
37
38#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
40pub enum RocmChannel {
41 Legacy,
43 Stable,
45 Latest,
47 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 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 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 pub fn configured() -> Self {
89 if let Ok(v) = std::env::var("ROCM_CHANNEL") {
92 if !v.trim().is_empty() {
93 return Self::parse(&v).unwrap_or(RocmChannel::Latest);
94 }
95 }
96 for home in Self::env_file_homes() {
97 if let Ok(body) = std::fs::read_to_string(std::path::Path::new(&home).join(".mlstack_env")) {
98 if let Some(found) = Self::channel_assignment(&body) {
99 return found.unwrap_or(RocmChannel::Latest);
100 }
101 }
102 }
103 RocmChannel::Latest
104 }
105
106 fn env_file_homes() -> Vec<String> {
110 let preferred = std::env::var("HOME").unwrap_or_else(|_| "/tmp".to_string());
113 crate::installer::candidate_mlstack_homes(&preferred)
114 }
115
116 pub fn channel_from_env_body(body: &str) -> Option<Self> {
121 Self::channel_assignment(body).flatten()
122 }
123
124 fn channel_assignment(body: &str) -> Option<Option<Self>> {
126 body.lines().rev().find_map(|l| {
129 let l = l.trim();
130 if l.starts_with('#') {
131 return None;
132 }
133 let stmt = l.rsplit(|c| c == ';').find(|p| p.contains("ROCM_CHANNEL")).unwrap_or(l);
138 let stmt = stmt.trim();
139 let stmt = ["then ", "or ", "and "]
140 .iter()
141 .find_map(|p| stmt.strip_prefix(p))
142 .unwrap_or(stmt)
143 .trim();
144 let raw = if let Some(r) = stmt
145 .strip_prefix("export ROCM_CHANNEL=")
146 .or_else(|| stmt.strip_prefix("ROCM_CHANNEL="))
147 {
148 r
149 } else if let Some(r) = stmt
150 .strip_prefix("export ROCM_CHANNEL")
153 .or_else(|| stmt.strip_prefix("ROCM_CHANNEL"))
154 .map(str::trim_start)
155 .and_then(|r| r.strip_prefix('='))
156 .map(str::trim_start)
157 {
158 r
159 } else if let Some(r) = stmt
160 .strip_prefix("set -gx ROCM_CHANNEL ")
161 .or_else(|| stmt.strip_prefix("set -x ROCM_CHANNEL "))
162 .or_else(|| stmt.strip_prefix("set -Ux ROCM_CHANNEL "))
163 {
164 r
165 } else {
166 return None;
167 };
168 let v = raw
169 .trim_start()
170 .split(|c: char| c.is_whitespace() || c == ';')
171 .next()
172 .unwrap_or("")
173 .trim_matches(|c| c == '"' || c == '\'');
174 Some(Self::parse(v))
175 })
176 }
177
178 pub fn for_version(version: &str) -> Self {
180 let mut it = version.split('.');
181 let major: u32 = it.next().and_then(|v| v.parse().ok()).unwrap_or(0);
182 let minor: u32 = it.next().and_then(|v| v.parse().ok()).unwrap_or(0);
183 let patch: u32 = it.next().and_then(|v| v.parse().ok()).unwrap_or(0);
184 match (major, minor) {
185 (10.., _) => RocmChannel::Preview,
186 (7, 14..) if patch >= 1 || minor > 14 => RocmChannel::Latest,
187 (7, 14..) => RocmChannel::Stable,
188 _ => RocmChannel::Legacy,
189 }
190 }
191
192 pub fn for_version_to_channel(version: &str) -> Option<Self> {
196 let v = version.trim();
197 [
198 RocmChannel::Legacy,
199 RocmChannel::Stable,
200 RocmChannel::Latest,
201 RocmChannel::Preview,
202 ]
203 .into_iter()
204 .find(|c| c.version() == v)
205 }
206
207 pub fn is_therock(&self) -> bool {
209 !matches!(self, RocmChannel::Legacy)
210 }
211
212 pub fn version(&self) -> &'static str {
214 match self {
215 RocmChannel::Legacy => "7.2.4",
216 RocmChannel::Stable => "7.14.0",
217 RocmChannel::Latest => "7.14.1",
218 RocmChannel::Preview => "10.1.0",
219 }
220 }
221
222 pub fn dir_path(&self) -> &'static str {
224 self.version()
225 }
226
227 pub fn pkg_version(&self) -> &'static str {
230 match self {
231 RocmChannel::Legacy => "7.2.4.70204-1",
232 RocmChannel::Stable => "7.14.0",
233 RocmChannel::Latest => "7.14.1",
234 RocmChannel::Preview => "10.1.0",
235 }
236 }
237
238 pub fn major_minor(&self) -> &'static str {
240 match self {
241 RocmChannel::Legacy => "7.2",
242 RocmChannel::Stable | RocmChannel::Latest => "7.14",
243 RocmChannel::Preview => "10.1",
244 }
245 }
246
247 pub fn version_from_manifest(manifest: &crate::core::manifest::Manifest) -> String {
252 manifest
253 .components
254 .iter()
255 .find(|c| c.id == "rocm")
256 .map(|c| c.version.clone())
257 .unwrap_or_else(|| RocmChannel::Latest.version().to_string())
258 }
259
260 pub fn pkg_version_from_manifest(manifest: &crate::core::manifest::Manifest) -> String {
265 let version = Self::version_from_manifest(manifest);
266 if version == "6.4.3" {
267 return "6.4.60403-1".to_string();
268 }
269 let parts: Vec<&str> = version.split('.').collect();
270 if parts.len() >= 3 {
271 let major: u64 = parts[0].parse().unwrap_or(7);
272 let minor: u64 = parts[1].parse().unwrap_or(2);
273 let patch: u64 = parts[2].parse().unwrap_or(0);
274 if RocmChannel::for_version(&version).is_therock() {
276 return version;
277 }
278 let build = format!("{major}{minor:02}{patch:02}");
279 format!("{}.{build}-1", version)
280 } else {
281 RocmChannel::Latest.pkg_version().to_string()
282 }
283 }
284}
285
286#[derive(Debug, Clone, Copy, PartialEq, Eq)]
288pub enum RocmInstallType {
289 Standard,
291 Minimal,
293 Full,
295 Custom,
297}
298
299impl fmt::Display for RocmInstallType {
300 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
301 match self {
302 RocmInstallType::Standard => write!(f, "standard"),
303 RocmInstallType::Minimal => write!(f, "minimal"),
304 RocmInstallType::Full => write!(f, "full"),
305 RocmInstallType::Custom => write!(f, "custom"),
306 }
307 }
308}
309
310#[derive(Debug, Clone)]
312pub struct RocmConfig {
313 pub channel: RocmChannel,
315 pub install_type: RocmInstallType,
317 pub force_reinstall: bool,
319 pub dry_run: bool,
321 pub gfx_arch: String,
323 pub legacy_fallback: bool,
325}
326
327impl Default for RocmConfig {
328 fn default() -> Self {
329 Self {
330 channel: RocmChannel::Latest,
331 install_type: RocmInstallType::Standard,
332 force_reinstall: false,
333 dry_run: false,
334 gfx_arch: "gfx1100".to_string(),
335 legacy_fallback: false,
336 }
337 }
338}
339
340#[derive(Debug, Clone, PartialEq, Eq)]
342pub struct PackageCommand {
343 pub program: String,
345 pub args: Vec<String>,
347}
348
349impl PackageCommand {
350 pub fn to_command_string(&self) -> String {
352 if self.args.is_empty() {
353 self.program.clone()
354 } else {
355 format!("{} {}", self.program, self.args.join(" "))
356 }
357 }
358}
359
360#[derive(Debug, Clone)]
362pub struct RepoConfig {
363 pub repo_type: String,
365 pub url: String,
367 pub gpg_key_url: String,
369 pub file_path: String,
371}
372
373pub struct RocmInstaller {
375 config: RocmConfig,
376}
377
378impl RocmInstaller {
379 pub fn new(config: RocmConfig) -> Self {
381 Self { config }
382 }
383
384 pub fn with_defaults() -> Self {
386 Self::new(RocmConfig::default())
387 }
388
389 pub fn channel(&self) -> RocmChannel {
391 self.config.channel
392 }
393
394 pub fn effective_version(&self) -> &'static str {
396 if self.config.channel == RocmChannel::Legacy && self.config.legacy_fallback {
397 LEGACY_FALLBACK_VERSION
398 } else {
399 self.config.channel.version()
400 }
401 }
402
403 fn classic_release(&self) -> &'static str {
404 self.effective_version()
405 }
406
407 fn classic_pkg_version(&self) -> &'static str {
408 if self.config.channel == RocmChannel::Legacy && self.config.legacy_fallback {
409 "7.2.3.70203-1"
410 } else {
411 self.config.channel.pkg_version()
412 }
413 }
414
415 pub fn therock_distro_dir(distro: &DistroFacade) -> Option<String> {
422 let v = distro.version();
423 let major = v.split('.').next().unwrap_or("");
424 if distro.uses_apt() {
425 if distro.id() == "debian" {
426 return match major {
427 "12" | "13" => Some(format!("debian{major}")),
428 _ => None,
429 };
430 }
431 let compact = v.replace('.', "");
432 match compact.as_str() {
433 "2204" | "2404" | "2604" => Some(format!("ubuntu{compact}")),
434 _ => match distro.codename() {
435 "jammy" => Some("ubuntu2204".into()),
436 "noble" => Some("ubuntu2404".into()),
437 _ => None,
438 },
439 }
440 } else if distro.uses_dnf() || distro.uses_yum() {
441 match major {
442 "8" | "9" | "10" => Some(format!("rhel{major}")),
443 _ => None,
444 }
445 } else if distro.uses_zypper() {
446 match major {
447 "15" => Some("sles15".into()),
448 "16" => Some("sles16".into()),
449 _ => None,
450 }
451 } else {
452 None
453 }
454 }
455
456 pub fn therock_package(&self) -> String {
458 format!(
459 "amdrocm{}-{}",
460 self.config.channel.major_minor(),
461 self.config.gfx_arch
462 )
463 }
464
465 pub fn therock_apt_version_glob(&self) -> String {
470 format!("{}-*", self.config.channel.version())
471 }
472
473 pub fn therock_apt_commands(
475 &self,
476 distro: &DistroFacade,
477 ) -> Result<Vec<PackageCommand>, String> {
478 let dir = Self::therock_distro_dir(distro).ok_or_else(|| {
479 format!(
480 "ROCm {} has no apt repo for {} {} (supported: Ubuntu 22.04/24.04/26.04, Debian 12/13)",
481 self.config.channel.version(),
482 distro.id(),
483 distro.version()
484 )
485 })?;
486 let sh = |script: String| PackageCommand {
487 program: "sudo".to_string(),
488 args: vec!["sh".to_string(), "-c".to_string(), script],
489 };
490 let key_script = format!(
493 "set -e; mkdir -p /etc/apt/keyrings; \
494 wget -qO /tmp/rocm-multi-arch.gpg {THEROCK_GPG_URL}; \
495 gpg --show-keys --with-colons /tmp/rocm-multi-arch.gpg | grep -q '^fpr:::::::::{THEROCK_GPG_FINGERPRINT}:' \
496 || {{ echo 'ROCm repo key fingerprint mismatch' >&2; rm -f /tmp/rocm-multi-arch.gpg; exit 1; }}; \
497 mv /tmp/rocm-multi-arch.gpg /etc/apt/keyrings/rocm-multi-arch.gpg"
498 );
499 Ok(vec![
500 sh(key_script),
501 sh(format!(
502 "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"
503 )),
504 PackageCommand {
505 program: "sudo".to_string(),
506 args: vec!["apt-get".to_string(), "update".to_string()],
507 },
508 PackageCommand {
509 program: "sudo".to_string(),
510 args: vec![
511 "apt-get".to_string(),
512 "install".to_string(),
513 "-y".to_string(),
514 format!("{}={}", self.therock_package(), self.therock_apt_version_glob()),
515 ],
516 },
517 ])
518 }
519
520 pub fn therock_rpm_commands(
522 &self,
523 distro: &DistroFacade,
524 ) -> Result<Vec<PackageCommand>, String> {
525 let dir = Self::therock_distro_dir(distro).ok_or_else(|| {
526 format!(
527 "ROCm {} has no rpm repo for {} {} (supported: RHEL 8/9/10, SLES 15/16)",
528 self.config.channel.version(),
529 distro.id(),
530 distro.version()
531 )
532 })?;
533 let url = format!("{THEROCK_REPO_ROOT}/{dir}/x86_64");
534 let pkg = format!(
535 "{}-{}",
536 self.therock_package(),
537 self.config.channel.version()
538 );
539 let sudo = |args: Vec<&str>| PackageCommand {
540 program: "sudo".to_string(),
541 args: args.into_iter().map(String::from).collect(),
542 };
543 let import_key = PackageCommand {
544 program: "sh".to_string(),
545 args: vec![
546 "-c".to_string(),
547 format!(
548 "set -e; wget -qO /tmp/rocm-multi-arch.gpg {THEROCK_GPG_URL}; \
549 gpg --show-keys --with-colons /tmp/rocm-multi-arch.gpg | grep -q '^fpr:::::::::{THEROCK_GPG_FINGERPRINT}:' \
550 || {{ echo 'ROCm GPG key fingerprint mismatch' >&2; exit 1; }}; \
551 sudo rpm --import /tmp/rocm-multi-arch.gpg"
552 ),
553 ],
554 };
555 Ok(if distro.uses_zypper() {
556 vec![
557 import_key,
558 sudo(vec![
559 "zypper",
560 "--non-interactive",
561 "addrepo",
562 "--refresh",
563 &url,
564 "rocm-multi-arch",
565 ]),
566 sudo(vec![
567 "zypper",
568 "--non-interactive",
569 "refresh",
570 ]),
571 sudo(vec!["zypper", "--non-interactive", "install", &pkg]),
572 ]
573 } else {
574 vec![
575 import_key,
576 sudo(vec!["dnf", "config-manager", "--add-repo", &url]),
577 sudo(vec!["dnf", "install", "-y", &pkg]),
578 ]
579 })
580 }
581
582 pub fn pip_requirement(&self) -> String {
585 format!(
586 "rocm[libraries,device-all]=={}",
587 self.config.channel.version()
588 )
589 }
590
591 pub fn pip_index_url(&self) -> Option<&'static str> {
593 match self.config.channel {
594 RocmChannel::Legacy => None,
595 RocmChannel::Stable | RocmChannel::Latest => Some(THEROCK_PIP_INDEX),
596 RocmChannel::Preview => Some(PREVIEW_PIP_INDEX),
597 }
598 }
599
600 pub fn therock_family(gfx: &str) -> Option<String> {
602 let g = gfx.trim().to_ascii_lowercase();
603 let fam = match g.as_str() {
604 s if s.starts_with("gfx101") => "gfx101X-dgpu",
605 s if s.starts_with("gfx103") => "gfx103X-all",
606 s if s.starts_with("gfx110") => "gfx110X-all",
607 s if s.starts_with("gfx115") => "gfx115X-all",
608 s if s.starts_with("gfx120") => "gfx120X-all",
609 "gfx908" => "gfx908",
610 "gfx90a" => "gfx90a",
611 "gfx942" | "gfx941" | "gfx940" => "gfx94X-dcgpu",
612 "gfx950" => "gfx950-dcgpu",
613 _ => return None,
614 };
615 Some(fam.to_string())
616 }
617
618 pub fn tarball_url(&self) -> Result<String, String> {
620 let family = Self::therock_family(&self.config.gfx_arch).ok_or_else(|| {
621 format!(
622 "no TheRock tarball family known for GPU architecture '{}'",
623 self.config.gfx_arch
624 )
625 })?;
626 Ok(format!(
627 "{THEROCK_TARBALL_ROOT}/therock-dist-linux-{family}-{}.tar.gz",
628 self.config.channel.version()
629 ))
630 }
631
632 pub fn runfile_url(&self) -> String {
637 let build = std::env::var("ROCM_RUNFILE_BUILD")
638 .ok()
639 .map(|b| b.trim().to_string())
640 .filter(|b| !b.is_empty() && b.chars().all(|c| c.is_ascii_digit()))
641 .unwrap_or_else(|| RUNFILE_BUILD_DEFAULT.to_string());
642 let v = self.config.channel.version();
643 let mm = self.config.channel.major_minor();
644 format!("{RUNFILE_ROOT}/rocm-rel-{mm}/rocm-installer-{v}-{build}.run")
645 }
646
647 pub fn repo_base_url(&self, distro: &DistroFacade) -> String {
662 let major_minor = self.config.channel.major_minor();
663 if distro.uses_apt() {
664 let codename = if distro.codename().is_empty() {
665 "noble"
666 } else {
667 distro.codename()
668 };
669 format!("https://repo.radeon.com/amdgpu/{major_minor}/ubuntu/{codename}")
670 } else if distro.uses_dnf() || distro.uses_yum() {
671 let rhel_version = Self::rhel_version(distro);
672 format!("https://repo.radeon.com/amdgpu/{major_minor}/rhel/{rhel_version}")
673 } else if distro.uses_zypper() {
674 format!("https://repo.radeon.com/amdgpu/{major_minor}/sle/15.7")
675 } else {
676 format!("https://repo.radeon.com/amdgpu/{major_minor}/ubuntu/noble")
678 }
679 }
680
681 pub fn rocm_core_repo_url(&self, _distro: &DistroFacade) -> String {
683 let major_minor = self.config.channel.major_minor();
684 format!("https://repo.radeon.com/rocm/apt/{major_minor}")
685 }
686
687 pub fn gpg_key_url(&self) -> &'static str {
689 "https://repo.radeon.com/rocm/rocm.gpg.key"
690 }
691
692 fn rhel_version(distro: &DistroFacade) -> &'static str {
694 let v = distro.version();
695 if v.starts_with("10") {
696 "10"
697 } else if v.starts_with("9") {
698 "9"
699 } else if v.starts_with("8") {
700 "8"
701 } else {
702 "9"
703 }
704 }
705
706 pub fn amdgpu_install_deb_url(&self) -> String {
712 let release_dir = self.classic_release();
713 let pkg_ver = self.classic_pkg_version();
714 format!(
715 "https://repo.radeon.com/amdgpu-install/{release_dir}/ubuntu/noble/amdgpu-install_{pkg_ver}_all.deb"
716 )
717 }
718
719 pub fn amdgpu_install_rpm_url(&self, rhel_version: &str) -> String {
721 let release_dir = self.classic_release();
722 let pkg_ver = self.classic_pkg_version();
723 format!(
724 "https://repo.radeon.com/amdgpu-install/{release_dir}/rhel/{rhel_version}/amdgpu-install-{pkg_ver}.el{rhel_version}.noarch.rpm"
725 )
726 }
727
728 pub fn amdgpu_install_sles_rpm_url(&self) -> String {
730 let release_dir = self.classic_release();
731 let pkg_ver = self.classic_pkg_version();
732 format!(
733 "https://repo.radeon.com/amdgpu-install/{release_dir}/sle/15.7/amdgpu-install-{pkg_ver}.noarch.rpm"
734 )
735 }
736
737 pub fn apt_install_commands(&self) -> Vec<PackageCommand> {
739 debug_assert!(
740 !self.config.channel.is_therock(),
741 "TheRock channels use therock_*_commands"
742 );
743 let deb_url = self.amdgpu_install_deb_url();
744 vec![
745 PackageCommand {
747 program: "wget".to_string(),
748 args: vec!["-q".to_string(), deb_url.clone()],
749 },
750 PackageCommand {
752 program: "sudo".to_string(),
753 args: vec![
754 "dpkg".to_string(),
755 "-i".to_string(),
756 format!("amdgpu-install_{}_all.deb", self.classic_pkg_version()),
757 ],
758 },
759 PackageCommand {
761 program: "sudo".to_string(),
762 args: vec!["apt-get".to_string(), "update".to_string()],
763 },
764 self.apt_rocm_metapackage_command(),
766 ]
767 }
768
769 fn apt_rocm_metapackage_command(&self) -> PackageCommand {
771 let metapkg = match self.config.install_type {
772 RocmInstallType::Standard => "rocm-libs",
773 RocmInstallType::Minimal => "rocm-core",
774 RocmInstallType::Full => "rocm-dev",
775 RocmInstallType::Custom => "rocm-libs",
776 };
777 PackageCommand {
778 program: "sudo".to_string(),
779 args: vec![
780 "apt-get".to_string(),
781 "install".to_string(),
782 "-y".to_string(),
783 metapkg.to_string(),
784 ],
785 }
786 }
787
788 pub fn dnf_install_commands(&self, rhel_version: &str) -> Vec<PackageCommand> {
790 debug_assert!(
791 !self.config.channel.is_therock(),
792 "TheRock channels use therock_*_commands"
793 );
794 let rpm_url = self.amdgpu_install_rpm_url(rhel_version);
795 vec![
796 PackageCommand {
797 program: "sudo".to_string(),
798 args: vec![
799 "dnf".to_string(),
800 "install".to_string(),
801 "-y".to_string(),
802 rpm_url,
803 ],
804 },
805 self.dnf_rocm_metapackage_command(),
806 ]
807 }
808
809 fn dnf_rocm_metapackage_command(&self) -> PackageCommand {
811 let metapkg = match self.config.install_type {
812 RocmInstallType::Standard => "rocm-libs",
813 RocmInstallType::Minimal => "rocm-core",
814 RocmInstallType::Full => "rocm-dev",
815 RocmInstallType::Custom => "rocm-libs",
816 };
817 PackageCommand {
818 program: "sudo".to_string(),
819 args: vec![
820 "dnf".to_string(),
821 "install".to_string(),
822 "-y".to_string(),
823 metapkg.to_string(),
824 ],
825 }
826 }
827
828 pub fn pacman_install_commands(&self, aur_helper: &str) -> Vec<PackageCommand> {
841 let packages = self.pacman_rocm_packages();
842 let mut args = vec![
843 "-S".to_string(),
844 "--needed".to_string(),
845 "--noconfirm".to_string(),
846 ];
847 args.extend(packages);
848
849 vec![PackageCommand {
850 program: aur_helper.to_string(),
851 args,
852 }]
853 }
854
855 pub fn pacman_rocm_packages(&self) -> Vec<String> {
857 let mut pkgs = vec![
858 "rocm-hip-sdk".to_string(),
859 "rocminfo".to_string(),
860 "rocm-smi-lib".to_string(),
861 "hipblas".to_string(),
862 "rocblas".to_string(),
863 "hipsparselt".to_string(),
864 ];
865 if self.config.channel != RocmChannel::Legacy {
867 pkgs.push("rocm-opencl-sdk".to_string());
868 pkgs.push("rccl".to_string());
869 pkgs.push("rocwmma".to_string());
873 }
874 pkgs
875 }
876
877 pub fn zypper_install_commands(&self) -> Vec<PackageCommand> {
879 debug_assert!(
880 !self.config.channel.is_therock(),
881 "TheRock channels use therock_*_commands"
882 );
883 let rpm_url = self.amdgpu_install_sles_rpm_url();
884 vec![
885 PackageCommand {
886 program: "sudo".to_string(),
887 args: vec![
888 "zypper".to_string(),
889 "--non-interactive".to_string(),
890 "install".to_string(),
891 rpm_url,
892 ],
893 },
894 PackageCommand {
895 program: "sudo".to_string(),
896 args: vec![
897 "zypper".to_string(),
898 "--non-interactive".to_string(),
899 "install".to_string(),
900 "-y".to_string(),
901 "rocm-libs".to_string(),
902 ],
903 },
904 ]
905 }
906
907 pub fn apt_repo_config(&self, distro: &DistroFacade) -> RepoConfig {
909 let codename = if distro.codename().is_empty() {
910 "noble"
911 } else {
912 distro.codename()
913 };
914 let major_minor = self.config.channel.major_minor();
915 RepoConfig {
916 repo_type: "apt".to_string(),
917 url: format!("deb [signed-by=/etc/apt/keyrings/rocm.gpg] https://repo.radeon.com/rocm/apt/{major_minor} {codename} main"),
918 gpg_key_url: "https://repo.radeon.com/rocm/rocm.gpg.key".to_string(),
919 file_path: "/etc/apt/sources.list.d/rocm.list".to_string(),
920 }
921 }
922
923 pub fn amdgpu_apt_repo_config(&self, distro: &DistroFacade) -> RepoConfig {
925 let codename = if distro.codename().is_empty() {
926 "noble"
927 } else {
928 distro.codename()
929 };
930 let major_minor = self.config.channel.major_minor();
931 RepoConfig {
932 repo_type: "apt".to_string(),
933 url: format!("deb [signed-by=/etc/apt/keyrings/rocm.gpg] https://repo.radeon.com/amdgpu/{major_minor}/ubuntu/{codename} {codename} main"),
934 gpg_key_url: "https://repo.radeon.com/rocm/rocm.gpg.key".to_string(),
935 file_path: "/etc/apt/sources.list.d/amdgpu.list".to_string(),
936 }
937 }
938
939 pub fn apt_pin_config(&self) -> (String, String) {
941 (
942 "Package: *\nPin: release o=repo.radeon.com\nPin-Priority: 600\n".to_string(),
943 "/etc/apt/preferences.d/rocm-pin-600".to_string(),
944 )
945 }
946
947 pub fn build_pacman_check_installed_command(package: &str) -> PackageCommand {
952 PackageCommand {
953 program: "pacman".to_string(),
954 args: vec!["-Qi".to_string(), package.to_string()],
955 }
956 }
957
958 pub fn filter_already_installed_pacman(packages: &[String]) -> Vec<String> {
964 use std::process::Command;
965
966 packages
967 .iter()
968 .filter(|pkg| {
969 let status = Command::new("pacman").args(["-Qi", pkg.as_str()]).output();
970 match status {
971 Ok(output) => !output.status.success(),
972 Err(_) => true, }
974 })
975 .cloned()
976 .collect()
977 }
978
979 pub fn is_up_to_date_output(output: &str) -> bool {
988 let lower = output.to_lowercase();
989 lower.contains("up to date -- skipping")
991 || lower.contains("up to date, skipping")
992 || lower.contains("up to date -- reinstalling")
993 || lower.contains("there is nothing to do")
994 || lower.contains("already installed")
995 || lower.contains("warning:") && lower.contains("up to date")
997 }
998}
999
1000#[cfg(test)]
1005mod tests {
1006 use super::*;
1007 use crate::platform::detection::{DistroFamily, DistroInfo, PackageManager};
1008
1009 fn ubuntu_distro() -> DistroFacade {
1010 DistroFacade::from_info(DistroInfo {
1011 id: "ubuntu".to_string(),
1012 name: "Ubuntu 24.04 LTS".to_string(),
1013 version: "24.04".to_string(),
1014 codename: "noble".to_string(),
1015 family: DistroFamily::Debian,
1016 pkg_manager: PackageManager::Apt,
1017 id_like: "debian".to_string(),
1018 })
1019 }
1020
1021 fn fedora_distro() -> DistroFacade {
1022 DistroFacade::from_info(DistroInfo {
1023 id: "fedora".to_string(),
1024 name: "Fedora Linux 41".to_string(),
1025 version: "41".to_string(),
1026 codename: String::new(),
1027 family: DistroFamily::Rhel,
1028 pkg_manager: PackageManager::Dnf,
1029 id_like: "fedora".to_string(),
1030 })
1031 }
1032
1033 fn rhel9_distro() -> DistroFacade {
1034 DistroFacade::from_info(DistroInfo {
1035 id: "rhel".to_string(),
1036 name: "Red Hat Enterprise Linux 9".to_string(),
1037 version: "9.3".to_string(),
1038 codename: String::new(),
1039 family: DistroFamily::Rhel,
1040 pkg_manager: PackageManager::Dnf,
1041 id_like: "rhel".to_string(),
1042 })
1043 }
1044
1045 fn suse_distro() -> DistroFacade {
1046 DistroFacade::from_info(DistroInfo {
1047 id: "opensuse-leap".to_string(),
1048 name: "openSUSE Leap 15.7".to_string(),
1049 version: "15.7".to_string(),
1050 codename: String::new(),
1051 family: DistroFamily::Suse,
1052 pkg_manager: PackageManager::Zypper,
1053 id_like: "suse".to_string(),
1054 })
1055 }
1056
1057 #[test]
1060 fn test_channel_versions() {
1061 assert_eq!(RocmChannel::Legacy.version(), "7.2.4");
1062 assert_eq!(RocmChannel::Legacy.pkg_version(), "7.2.4.70204-1");
1063 assert_eq!(RocmChannel::Legacy.major_minor(), "7.2");
1064 assert_eq!(RocmChannel::Stable.version(), "7.14.0");
1065 assert_eq!(RocmChannel::Latest.version(), "7.14.1");
1066 assert_eq!(RocmChannel::Stable.major_minor(), "7.14");
1067 assert_eq!(RocmChannel::Preview.version(), "10.1.0");
1068 assert_eq!(RocmChannel::Preview.major_minor(), "10.1");
1069 assert!(!RocmChannel::Legacy.is_therock());
1070 assert!(RocmChannel::Preview.is_therock());
1071 }
1072
1073 #[test]
1074 fn test_channel_from_choice() {
1075 assert_eq!(RocmChannel::from_choice(1), Some(RocmChannel::Legacy));
1076 assert_eq!(RocmChannel::from_choice(2), Some(RocmChannel::Stable));
1077 assert_eq!(RocmChannel::from_choice(3), Some(RocmChannel::Latest));
1078 assert_eq!(RocmChannel::from_choice(4), Some(RocmChannel::Preview));
1079 assert_eq!(RocmChannel::from_choice(0), None);
1080 assert_eq!(RocmChannel::from_choice(5), None);
1081 }
1082
1083 #[test]
1084 fn test_channel_parse_and_for_version() {
1085 assert_eq!(RocmChannel::parse("Preview"), Some(RocmChannel::Preview));
1086 assert_eq!(RocmChannel::parse("bogus"), None);
1087 assert_eq!(RocmChannel::for_version("10.1.0"), RocmChannel::Preview);
1088 assert_eq!(RocmChannel::for_version("7.14.1"), RocmChannel::Latest);
1089 assert_eq!(RocmChannel::for_version("7.14.0"), RocmChannel::Stable);
1090 assert_eq!(RocmChannel::for_version("7.2.4"), RocmChannel::Legacy);
1091 assert_eq!(RocmChannel::for_version("6.4.3"), RocmChannel::Legacy);
1092 }
1093
1094 #[test]
1095 fn test_legacy_fallback_723() {
1096 let i = RocmInstaller::new(RocmConfig {
1097 channel: RocmChannel::Legacy,
1098 legacy_fallback: true,
1099 ..Default::default()
1100 });
1101 assert_eq!(i.effective_version(), "7.2.3");
1102 assert!(i
1103 .amdgpu_install_deb_url()
1104 .contains("7.2.3/ubuntu/noble/amdgpu-install_7.2.3.70203-1_all.deb"));
1105 }
1106
1107 #[test]
1108 fn test_therock_urls_and_package() {
1109 let i = RocmInstaller::new(RocmConfig {
1110 channel: RocmChannel::Stable,
1111 gfx_arch: "gfx1100".into(),
1112 ..Default::default()
1113 });
1114 assert_eq!(i.therock_package(), "amdrocm7.14-gfx1100");
1115 assert_eq!(i.pip_requirement(), "rocm[libraries,device-all]==7.14.0");
1116 assert_eq!(
1117 i.pip_index_url(),
1118 Some("https://repo.amd.com/rocm/whl-multi-arch/")
1119 );
1120 assert_eq!(
1121 i.tarball_url().unwrap(),
1122 "https://repo.amd.com/rocm/tarball-multi-arch/therock-dist-linux-gfx110X-all-7.14.0.tar.gz"
1123 );
1124 assert_eq!(
1125 i.runfile_url(),
1126 "https://repo.radeon.com/installer/rocm-runfile-installer/rocm-rel-7.14/rocm-installer-7.14.0-7.run"
1127 );
1128 }
1129
1130 #[test]
1131 fn test_therock_apt_commands_latest() {
1132 let i = RocmInstaller::new(RocmConfig {
1133 channel: RocmChannel::Latest,
1134 ..Default::default()
1135 });
1136 let cmds = i.therock_apt_commands(&ubuntu_distro()).unwrap();
1137 let all: String = cmds
1138 .iter()
1139 .map(|c| c.to_command_string())
1140 .collect::<Vec<_>>()
1141 .join("\n");
1142 assert!(all.contains("packages-multi-arch/gpg/rocm.gpg"));
1143 assert!(all.contains("packages-multi-arch/ubuntu2404 stable main"));
1144 assert!(all.contains("amdrocm7.14-gfx1100=7.14.1-*"));
1145 }
1146
1147 #[test]
1148 fn test_therock_rpm_and_distro_dir() {
1149 let i = RocmInstaller::new(RocmConfig {
1150 channel: RocmChannel::Latest,
1151 ..Default::default()
1152 });
1153 assert_eq!(
1154 RocmInstaller::therock_distro_dir(&ubuntu_distro()).as_deref(),
1155 Some("ubuntu2404")
1156 );
1157 assert_eq!(
1158 RocmInstaller::therock_distro_dir(&rhel9_distro()).as_deref(),
1159 Some("rhel9")
1160 );
1161 let cmds = i.therock_rpm_commands(&rhel9_distro()).unwrap();
1162 assert!(cmds
1163 .iter()
1164 .any(|c| c.to_command_string().contains("amdrocm7.14-gfx1100-7.14.1")));
1165 }
1166
1167 #[test]
1168 fn test_therock_unknown_gpu_and_distro_error() {
1169 let i = RocmInstaller::new(RocmConfig {
1170 channel: RocmChannel::Latest,
1171 gfx_arch: "gfx9999".into(),
1172 ..Default::default()
1173 });
1174 assert!(i.tarball_url().is_err());
1175 assert_eq!(RocmInstaller::therock_family("gfx9999"), None);
1176 }
1177
1178 #[test]
1179 fn test_therock_apt_pins_gpg_fingerprint() {
1180 let i = RocmInstaller::new(RocmConfig {
1181 channel: RocmChannel::Latest,
1182 ..Default::default()
1183 });
1184 let all: String = i
1185 .therock_apt_commands(&ubuntu_distro())
1186 .unwrap()
1187 .iter()
1188 .map(|c| c.to_command_string())
1189 .collect();
1190 assert!(all.contains(THEROCK_GPG_FINGERPRINT));
1191 }
1192
1193 #[test]
1194 fn test_pkg_version_classification_matches_for_version() {
1195 for v in ["8.0.0", "9.1.0", "7.2.4", "7.13.0"] {
1196 assert!(!RocmChannel::for_version(v).is_therock(), "{v}");
1197 }
1198 for v in ["7.14.0", "7.14.1", "10.1.0"] {
1199 assert!(RocmChannel::for_version(v).is_therock(), "{v}");
1200 }
1201 }
1202
1203 #[test]
1204 fn test_preview_channel_consistency() {
1205 let ch = RocmChannel::from_choice(4).unwrap();
1206 assert_eq!(ch, RocmChannel::Preview);
1207 assert_eq!(ch.version(), "10.1.0");
1208 assert_eq!(ch.major_minor(), "10.1");
1209 let i = RocmInstaller::new(RocmConfig {
1210 channel: ch,
1211 ..Default::default()
1212 });
1213 assert_eq!(i.pip_requirement(), "rocm[libraries,device-all]==10.1.0");
1214 }
1215
1216 #[test]
1217 fn test_preview_pip_index() {
1218 let i = RocmInstaller::new(RocmConfig {
1219 channel: RocmChannel::Preview,
1220 ..Default::default()
1221 });
1222 assert_eq!(
1223 i.pip_index_url(),
1224 Some("https://stable.repo.amd.com/rocm/whl-next/")
1225 );
1226 assert_eq!(i.pip_requirement(), "rocm[libraries,device-all]==10.1.0");
1227 }
1228
1229 #[test]
1230 fn test_pkg_version_from_manifest_latest() {
1231 let manifest = crate::core::manifest::Manifest {
1232 schema_version: 2,
1233 sequence: 1,
1234 generated_at: "2026-05-31T00:00:00Z".to_string(),
1235 expires_at: None,
1236 min_runtime_version: String::new(),
1237 components: vec![crate::core::manifest::ManifestComponent {
1238 id: "rocm".to_string(),
1239 version: "7.2.4".to_string(),
1240 script: "scripts/install_rocm.sh".to_string(),
1241 category: crate::core::Category::Foundation,
1242 validation_tier: crate::core::ValidationTier::Validated,
1243 min_rocm_version: String::new(),
1244 dependencies: vec![],
1245 compatible_channels: vec!["latest".to_string()],
1246 exclusive_group: String::new(),
1247 }],
1248 signature: None,
1249 };
1250
1251 assert_eq!(
1252 RocmChannel::pkg_version_from_manifest(&manifest),
1253 "7.2.4.70204-1"
1254 );
1255 }
1256
1257 #[test]
1258 fn test_pkg_version_from_manifest_legacy() {
1259 let manifest = crate::core::manifest::Manifest {
1260 schema_version: 2,
1261 sequence: 1,
1262 generated_at: "2026-05-31T00:00:00Z".to_string(),
1263 expires_at: None,
1264 min_runtime_version: String::new(),
1265 components: vec![crate::core::manifest::ManifestComponent {
1266 id: "rocm".to_string(),
1267 version: "6.4.3".to_string(),
1268 script: "scripts/install_rocm.sh".to_string(),
1269 category: crate::core::Category::Foundation,
1270 validation_tier: crate::core::ValidationTier::Validated,
1271 min_rocm_version: String::new(),
1272 dependencies: vec![],
1273 compatible_channels: vec!["legacy".to_string()],
1274 exclusive_group: String::new(),
1275 }],
1276 signature: None,
1277 };
1278
1279 assert_eq!(
1280 RocmChannel::pkg_version_from_manifest(&manifest),
1281 "6.4.60403-1"
1282 );
1283 }
1284
1285 #[test]
1288 fn test_apt_install_commands_latest() {
1289 let installer = RocmInstaller::new(RocmConfig {
1290 channel: RocmChannel::Legacy,
1291 install_type: RocmInstallType::Standard,
1292 ..Default::default()
1293 });
1294 let cmds = installer.apt_install_commands();
1295 assert_eq!(cmds.len(), 4);
1296
1297 assert_eq!(cmds[0].program, "wget");
1299 assert!(cmds[0]
1300 .args
1301 .iter()
1302 .any(|a| a.contains("7.2.4/ubuntu/noble")));
1303 assert!(cmds[0].args.iter().any(|a| a.contains("7.2.4.70204-1")));
1304 assert_eq!(cmds[1].program, "sudo");
1305 assert!(cmds[1].args.contains(&"dpkg".to_string()));
1306
1307 assert_eq!(cmds[2].program, "sudo");
1309 assert!(cmds[2].args.contains(&"update".to_string()));
1310
1311 assert_eq!(cmds[3].program, "sudo");
1313 assert!(cmds[3].args.contains(&"rocm-libs".to_string()));
1314 }
1315
1316 #[test]
1317 fn test_apt_install_commands_full() {
1318 let installer = RocmInstaller::new(RocmConfig {
1319 channel: RocmChannel::Legacy,
1320 install_type: RocmInstallType::Full,
1321 ..Default::default()
1322 });
1323 let cmds = installer.apt_install_commands();
1324 let meta_cmd = &cmds[3];
1325 assert!(meta_cmd.args.contains(&"rocm-dev".to_string()));
1326 }
1327
1328 #[test]
1329 fn test_dnf_install_commands() {
1330 let installer = RocmInstaller::new(RocmConfig {
1331 channel: RocmChannel::Legacy,
1332 install_type: RocmInstallType::Standard,
1333 ..Default::default()
1334 });
1335 let cmds = installer.dnf_install_commands("9");
1336 assert_eq!(cmds.len(), 2);
1337
1338 assert_eq!(cmds[0].program, "sudo");
1340 assert!(cmds[0].args.iter().any(|a| a.contains("7.2.4/rhel/9")));
1341 assert!(cmds[0].args.iter().any(|a| a.contains("7.2.4.70204-1")));
1342 assert!(cmds[0].args.iter().any(|a| a.contains("el9")));
1343
1344 assert_eq!(cmds[1].program, "sudo");
1346 assert!(cmds[1].args.contains(&"rocm-libs".to_string()));
1347 }
1348
1349 #[test]
1350 fn test_pacman_install_commands() {
1351 let installer = RocmInstaller::new(RocmConfig {
1352 channel: RocmChannel::Latest,
1353 install_type: RocmInstallType::Standard,
1354 ..Default::default()
1355 });
1356 let cmds = installer.pacman_install_commands("yay");
1357 assert_eq!(
1362 cmds.len(),
1363 1,
1364 "pacman install must be a single user-space yay command"
1365 );
1366 assert_eq!(cmds[0].program, "yay");
1367 assert!(
1368 !cmds.iter().any(|c| c.program == "sudo"),
1369 "yay must not run under sudo"
1370 );
1371 assert!(cmds[0].args.contains(&"-S".to_string()));
1372 assert!(cmds[0].args.contains(&"--needed".to_string()));
1373 assert!(cmds[0].args.contains(&"--noconfirm".to_string()));
1374 let pkgs = installer.pacman_rocm_packages();
1376 assert!(pkgs.contains(&"rocm-hip-sdk".to_string()));
1377 assert!(pkgs.contains(&"rocminfo".to_string()));
1378 assert!(pkgs.contains(&"rocm-opencl-sdk".to_string())); assert!(pkgs.contains(&"rccl".to_string())); assert!(pkgs.contains(&"rocwmma".to_string())); for p in &pkgs {
1382 assert!(cmds[0].args.contains(p), "yay args missing package {p}");
1383 }
1384 }
1385
1386 #[test]
1387 fn test_pacman_legacy_no_extras() {
1388 let installer = RocmInstaller::new(RocmConfig {
1389 channel: RocmChannel::Legacy,
1390 install_type: RocmInstallType::Standard,
1391 ..Default::default()
1392 });
1393 let pkgs = installer.pacman_rocm_packages();
1394 assert!(!pkgs.contains(&"rocm-opencl-sdk".to_string()));
1395 assert!(!pkgs.contains(&"rccl".to_string()));
1396 assert!(!pkgs.contains(&"rocwmma".to_string())); }
1398
1399 #[test]
1400 fn test_zypper_install_commands() {
1401 let installer = RocmInstaller::new(RocmConfig {
1402 channel: RocmChannel::Legacy,
1403 install_type: RocmInstallType::Standard,
1404 ..Default::default()
1405 });
1406 let cmds = installer.zypper_install_commands();
1407 assert_eq!(cmds.len(), 2);
1408 assert_eq!(cmds[0].program, "sudo");
1409 assert!(cmds[0].args.iter().any(|a| a.contains("zypper")));
1410 assert!(cmds[0].args.iter().any(|a| a.contains("7.2.4/sle/15.7")));
1411 assert!(cmds[0].args.iter().any(|a| a.contains("7.2.4.70204-1")));
1412 }
1413
1414 #[test]
1417 fn test_repo_base_url_ubuntu() {
1418 let installer = RocmInstaller::new(RocmConfig {
1419 channel: RocmChannel::Legacy,
1420 ..Default::default()
1421 });
1422 let distro = ubuntu_distro();
1423 let url = installer.repo_base_url(&distro);
1424 assert_eq!(url, "https://repo.radeon.com/amdgpu/7.2/ubuntu/noble");
1425 }
1426
1427 #[test]
1428 fn test_repo_base_url_rhel() {
1429 let installer = RocmInstaller::new(RocmConfig {
1430 channel: RocmChannel::Legacy,
1431 ..Default::default()
1432 });
1433 let distro = rhel9_distro();
1434 let url = installer.repo_base_url(&distro);
1435 assert_eq!(url, "https://repo.radeon.com/amdgpu/7.2/rhel/9");
1436 }
1437
1438 #[test]
1439 fn test_repo_base_url_fedora() {
1440 let installer = RocmInstaller::new(RocmConfig {
1441 channel: RocmChannel::Legacy,
1442 ..Default::default()
1443 });
1444 let distro = fedora_distro();
1445 let url = installer.repo_base_url(&distro);
1446 assert_eq!(url, "https://repo.radeon.com/amdgpu/7.2/rhel/9");
1448 }
1449
1450 #[test]
1451 fn test_repo_base_url_suse() {
1452 let installer = RocmInstaller::new(RocmConfig {
1453 channel: RocmChannel::Legacy,
1454 ..Default::default()
1455 });
1456 let distro = suse_distro();
1457 let url = installer.repo_base_url(&distro);
1458 assert_eq!(url, "https://repo.radeon.com/amdgpu/7.2/sle/15.7");
1459 }
1460
1461 #[test]
1462 fn test_amdgpu_install_deb_url() {
1463 let installer = RocmInstaller::new(RocmConfig {
1464 channel: RocmChannel::Legacy,
1465 ..Default::default()
1466 });
1467 let url = installer.amdgpu_install_deb_url();
1468 assert!(url.contains("7.2.4/ubuntu/noble"));
1469 assert!(url.contains("7.2.4.70204-1"));
1470 assert!(url.contains("ubuntu/noble"));
1471 assert!(url.ends_with(".deb"));
1472 }
1473
1474 #[test]
1475 fn test_amdgpu_install_rpm_url() {
1476 let installer = RocmInstaller::new(RocmConfig {
1477 channel: RocmChannel::Legacy,
1478 ..Default::default()
1479 });
1480 let url = installer.amdgpu_install_rpm_url("9");
1481 assert!(url.contains("7.2.4/rhel/9"));
1482 assert!(url.contains("7.2.4.70204-1"));
1483 assert!(url.contains("rhel/9"));
1484 assert!(url.ends_with(".noarch.rpm"));
1485 }
1486
1487 #[test]
1488 fn test_apt_repo_config() {
1489 let installer = RocmInstaller::new(RocmConfig {
1490 channel: RocmChannel::Legacy,
1491 ..Default::default()
1492 });
1493 let distro = ubuntu_distro();
1494 let repo = installer.apt_repo_config(&distro);
1495 assert_eq!(repo.file_path, "/etc/apt/sources.list.d/rocm.list");
1496 assert!(repo.url.contains("rocm/apt/7.2"));
1497 assert!(repo.url.contains("noble"));
1498 }
1499
1500 #[test]
1501 fn test_amdgpu_apt_repo_config() {
1502 let installer = RocmInstaller::new(RocmConfig {
1503 channel: RocmChannel::Legacy,
1504 ..Default::default()
1505 });
1506 let distro = ubuntu_distro();
1507 let repo = installer.amdgpu_apt_repo_config(&distro);
1508 assert_eq!(repo.file_path, "/etc/apt/sources.list.d/amdgpu.list");
1509 assert!(repo.url.contains("amdgpu/7.2"));
1510 }
1511
1512 #[test]
1513 fn test_apt_pin_config() {
1514 let installer = RocmInstaller::with_defaults();
1515 let (content, path) = installer.apt_pin_config();
1516 assert_eq!(path, "/etc/apt/preferences.d/rocm-pin-600");
1517 assert!(content.contains("Pin-Priority: 600"));
1518 }
1519
1520 #[test]
1523 fn test_all_channels_version_consistency() {
1524 for choice in [1u8, 2, 3] {
1525 let ch = RocmChannel::from_choice(choice).unwrap();
1526 let config = RocmConfig {
1527 channel: ch,
1528 ..Default::default()
1529 };
1530 let installer = RocmInstaller::new(config);
1531
1532 let deb_url = installer.amdgpu_install_deb_url();
1534 assert!(
1535 deb_url.contains(ch.pkg_version()),
1536 "deb URL for {:?} should contain pkg_version {}",
1537 ch,
1538 ch.pkg_version()
1539 );
1540
1541 let distro = ubuntu_distro();
1543 let repo_url = installer.repo_base_url(&distro);
1544 assert!(
1545 repo_url.contains(ch.major_minor()),
1546 "repo URL for {:?} should contain major_minor {}",
1547 ch,
1548 ch.major_minor()
1549 );
1550 }
1551 }
1552
1553 #[test]
1554 fn test_install_type_metapackages() {
1555 for (itype, expected_pkg) in [
1556 (RocmInstallType::Standard, "rocm-libs"),
1557 (RocmInstallType::Minimal, "rocm-core"),
1558 (RocmInstallType::Full, "rocm-dev"),
1559 (RocmInstallType::Custom, "rocm-libs"),
1560 ] {
1561 let installer = RocmInstaller::new(RocmConfig {
1562 channel: RocmChannel::Legacy,
1563 install_type: itype,
1564 ..Default::default()
1565 });
1566 let cmds = installer.apt_install_commands();
1567 let meta_cmd = &cmds[3];
1568 assert!(
1569 meta_cmd.args.contains(&expected_pkg.to_string()),
1570 "Install type {:?} should use metapackage {}",
1571 itype,
1572 expected_pkg
1573 );
1574 }
1575 }
1576
1577 #[test]
1580 fn test_is_up_to_date_yay_warning() {
1581 assert!(RocmInstaller::is_up_to_date_output(
1582 "warning: rocminfo is up to date -- skipping"
1583 ));
1584 }
1585
1586 #[test]
1587 fn test_is_up_to_date_yay_warning_case_insensitive() {
1588 assert!(RocmInstaller::is_up_to_date_output(
1589 "WARNING: rocminfo is UP TO DATE -- skipping"
1590 ));
1591 }
1592
1593 #[test]
1594 fn test_is_up_to_date_nothing_to_do() {
1595 assert!(RocmInstaller::is_up_to_date_output(
1596 " there is nothing to do\n"
1597 ));
1598 }
1599
1600 #[test]
1601 fn test_is_up_to_date_already_installed() {
1602 assert!(RocmInstaller::is_up_to_date_output(
1603 "package already installed"
1604 ));
1605 }
1606
1607 #[test]
1608 fn test_is_up_to_date_normal_output_is_false() {
1609 assert!(!RocmInstaller::is_up_to_date_output(
1610 "resolving dependencies...\nlooking for conflicting packages...\n"
1611 ));
1612 }
1613
1614 #[test]
1615 fn test_is_up_to_date_error_output_is_false() {
1616 assert!(!RocmInstaller::is_up_to_date_output(
1617 "error: failed to prepare transaction\nerror: target not found: rocminfo"
1618 ));
1619 }
1620
1621 #[test]
1622 fn test_is_up_to_date_empty_output_is_false() {
1623 assert!(!RocmInstaller::is_up_to_date_output(""));
1624 }
1625
1626 #[test]
1627 fn test_is_up_to_date_pacman_skipping() {
1628 assert!(RocmInstaller::is_up_to_date_output(
1629 "warning: rocm-hip-sdk is up to date -- skipping"
1630 ));
1631 }
1632
1633 #[test]
1636 fn test_build_pacman_check_installed_command() {
1637 let cmd = RocmInstaller::build_pacman_check_installed_command("rocminfo");
1638 assert_eq!(cmd.program, "pacman");
1639 assert_eq!(cmd.args, vec!["-Qi", "rocminfo"]);
1640 }
1641
1642 #[test]
1643 fn test_build_pacman_check_installed_command_complex_pkg() {
1644 let cmd = RocmInstaller::build_pacman_check_installed_command("rocm-hip-sdk");
1645 assert_eq!(cmd.program, "pacman");
1646 assert!(cmd.args.contains(&"rocm-hip-sdk".to_string()));
1647 }
1648}
1649
1650#[cfg(test)]
1651mod channel_env_tests {
1652 use super::RocmChannel;
1653
1654 #[test]
1655 fn parses_plain_guarded_and_fish() {
1656 let f = RocmChannel::channel_from_env_body;
1657 assert_eq!(f("export ROCM_CHANNEL=stable\n"), Some(RocmChannel::Stable));
1658 assert_eq!(
1659 f("if [ -z \"${ROCM_CHANNEL:-}\" ]; then export ROCM_CHANNEL=legacy; fi"),
1660 Some(RocmChannel::Legacy)
1661 );
1662 assert_eq!(f("set -q ROCM_CHANNEL; or set -gx ROCM_CHANNEL preview"), Some(RocmChannel::Preview));
1663 assert_eq!(f("# export ROCM_CHANNEL=stable"), None);
1664 assert_eq!(f("export ROCM_CHANNEL=bogus"), None);
1665 assert_eq!(f("export ROCM_CHANNEL = stable"), Some(RocmChannel::Stable));
1667 assert_eq!(f("ROCM_CHANNEL = \"latest\""), Some(RocmChannel::Latest));
1668 assert_eq!(f("export ROCM_CHANNEL_X=stable"), None);
1670 assert_eq!(f("grep ROCM_CHANNEL env"), None);
1671 }
1672
1673 #[test]
1674 fn channel_env_last_invalid_assignment_wins() {
1675 assert_eq!(
1676 RocmChannel::channel_from_env_body("export ROCM_CHANNEL=stable\nexport ROCM_CHANNEL=bogus\n"),
1677 None
1678 );
1679 }
1680
1681 #[test]
1682 fn invalid_assignment_is_distinct_from_missing() {
1683 assert_eq!(RocmChannel::channel_assignment("export ROCM_CHANNEL=bogus\n"), Some(None));
1684 assert_eq!(RocmChannel::channel_assignment("echo hi\n"), None);
1685 }
1686
1687 #[test]
1688 fn last_assignment_wins() {
1689 assert_eq!(
1690 RocmChannel::channel_from_env_body("export ROCM_CHANNEL=stable\necho ROCM_CHANNEL=latest\n"),
1691 Some(RocmChannel::Stable)
1692 );
1693 assert_eq!(
1694 RocmChannel::channel_from_env_body("export ROCM_CHANNEL=stable\nexport ROCM_CHANNEL=\"latest\"\n"),
1695 Some(RocmChannel::Latest)
1696 );
1697 }
1698
1699 #[test]
1700 fn version_round_trips_to_its_channel() {
1701 for channel in [
1704 RocmChannel::Legacy,
1705 RocmChannel::Stable,
1706 RocmChannel::Latest,
1707 RocmChannel::Preview,
1708 ] {
1709 assert_eq!(
1710 RocmChannel::for_version_to_channel(channel.version()),
1711 Some(channel)
1712 );
1713 }
1714 assert_eq!(RocmChannel::for_version_to_channel("9.9.9"), None);
1716 assert_eq!(RocmChannel::for_version_to_channel("latest"), None);
1717 assert_eq!(RocmChannel::for_version_to_channel(" 7.14.1 "), Some(RocmChannel::Latest));
1718 }
1719}