Skip to main content

rusty_stack/installers/components/
amdgpu_drivers.rs

1//! AMD GPU drivers installer — ports `scripts/install_amdgpu_drivers.sh`.
2//!
3//! Constructs correct package commands per distro family for AMDGPU driver
4//! installation.
5//!
6//! # Validation Assertions
7//!
8//! - **VAL-INSTALL-029**: AMD GPU drivers correct package command
9
10use crate::installers::common::DistroFacade;
11use crate::platform::detection::DistroFamily;
12
13// ===========================================================================
14// Types
15// ===========================================================================
16
17/// A constructed shell command.
18#[derive(Debug, Clone, PartialEq, Eq)]
19pub struct ShellCommand {
20    /// The program to run.
21    pub program: String,
22    /// Arguments to pass.
23    pub args: Vec<String>,
24    /// Environment variables to set.
25    pub env: Vec<(String, String)>,
26}
27
28impl ShellCommand {
29    /// Format as a shell command string.
30    pub fn to_command_string(&self) -> String {
31        let env_prefix = self
32            .env
33            .iter()
34            .map(|(k, v)| format!("{k}={v}"))
35            .collect::<Vec<_>>()
36            .join(" ");
37        let cmd = if self.args.is_empty() {
38            self.program.clone()
39        } else {
40            format!("{} {}", self.program, self.args.join(" "))
41        };
42        if env_prefix.is_empty() {
43            cmd
44        } else {
45            format!("{env_prefix} {cmd}")
46        }
47    }
48}
49
50/// Configuration for the AMDGPU drivers installer.
51#[derive(Debug, Clone, Default)]
52pub struct AmdgpuConfig {
53    /// Whether to run in dry-run mode.
54    pub dry_run: bool,
55    /// Whether to force reinstallation.
56    pub force: bool,
57}
58
59/// The AMDGPU drivers installer.
60pub struct AmdgpuInstaller {
61    config: AmdgpuConfig,
62}
63
64impl AmdgpuInstaller {
65    /// Create a new AMDGPU drivers installer with the given config.
66    pub fn new(config: AmdgpuConfig) -> Self {
67        Self { config }
68    }
69
70    /// Create with default config.
71    pub fn with_defaults() -> Self {
72        Self::new(AmdgpuConfig::default())
73    }
74
75    // -----------------------------------------------------------------------
76    // Package commands per distro (VAL-INSTALL-029)
77    // -----------------------------------------------------------------------
78
79    /// Get the required system packages for the given distro.
80    pub fn required_packages(&self, distro: &DistroFacade) -> Vec<&str> {
81        match distro.family() {
82            DistroFamily::Debian => vec![
83                "amdgpu-dkms",
84                "linux-headers-$(uname -r)",
85                "linux-modules-extra-$(uname -r)",
86            ],
87            DistroFamily::Rhel => vec!["amdgpu-dkms", "kernel-devel"],
88            DistroFamily::Arch => vec!["amdgpu"],
89            DistroFamily::Suse => vec!["amdgpu-dkms", "kernel-default-devel"],
90            _ => vec!["amdgpu-dkms"],
91        }
92    }
93
94    /// Get the ROCm packages for the given distro.
95    pub fn rocm_packages(&self, _distro: &DistroFacade) -> Vec<&str> {
96        vec!["rocm-dev", "rocm-libs", "rocprofiler"]
97    }
98
99    /// Construct the package install command for the given distro.
100    ///
101    /// The original script uses:
102    /// - apt: `sudo apt install -y <packages>`
103    /// - dnf: `sudo dnf install -y <packages>`
104    /// - pacman: `sudo pacman -S --noconfirm <packages>`
105    /// - zypper: `sudo zypper install -y <packages>`
106    pub fn build_package_install_command(
107        &self,
108        distro: &DistroFacade,
109        packages: &[&str],
110    ) -> ShellCommand {
111        let (program, mut base_args) = match distro.family() {
112            DistroFamily::Debian => (
113                "sudo".to_string(),
114                vec![
115                    "apt-get".to_string(),
116                    "install".to_string(),
117                    "-y".to_string(),
118                ],
119            ),
120            DistroFamily::Rhel => (
121                "sudo".to_string(),
122                vec!["dnf".to_string(), "install".to_string(), "-y".to_string()],
123            ),
124            DistroFamily::Arch => (
125                "sudo".to_string(),
126                vec![
127                    "pacman".to_string(),
128                    "-S".to_string(),
129                    "--noconfirm".to_string(),
130                ],
131            ),
132            DistroFamily::Suse => (
133                "sudo".to_string(),
134                vec![
135                    "zypper".to_string(),
136                    "install".to_string(),
137                    "-y".to_string(),
138                ],
139            ),
140            _ => (
141                "sudo".to_string(),
142                vec![
143                    "apt-get".to_string(),
144                    "install".to_string(),
145                    "-y".to_string(),
146                ],
147            ),
148        };
149
150        for pkg in packages {
151            base_args.push(pkg.to_string());
152        }
153
154        // Honor dry-run: echo the command that would run instead of executing it.
155        if self.config.dry_run {
156            let mut preview = base_args;
157            preview.insert(0, program.clone());
158            return ShellCommand {
159                program: "echo".to_string(),
160                args: preview,
161                env: vec![],
162            };
163        }
164
165        ShellCommand {
166            program,
167            args: base_args,
168            env: vec![],
169        }
170    }
171
172    /// Construct the apt-specific download and install command for amdgpu-install.
173    ///
174    /// The original script (Ubuntu-specific):
175    /// 1. `wget https://repo.radeon.com/amdgpu-install/7.2.4/ubuntu/$CODENAME/amdgpu-install_7.2.4.70204-1_all.deb`
176    /// 2. `sudo apt install -y ./amdgpu-install_7.2.4.70204-1_all.deb`
177    /// 3. `sudo apt update`
178    /// 4. `sudo apt install -y linux-headers-$(uname -r) linux-modules-extra-$(uname -r)`
179    /// 5. `sudo apt install -y amdgpu-dkms`
180    pub fn build_amdgpu_install_deb_commands(&self, codename: &str) -> Vec<ShellCommand> {
181        let deb_url = format!(
182            "https://repo.radeon.com/amdgpu-install/7.2.4/ubuntu/{}/amdgpu-install_7.2.4.70204-1_all.deb",
183            codename
184        );
185        let deb_file = "amdgpu-install_7.2.4.70204-1_all.deb";
186
187        vec![
188            // Download
189            ShellCommand {
190                program: "wget".to_string(),
191                args: vec!["-q".to_string(), deb_url],
192                env: vec![],
193            },
194            // Install deb
195            ShellCommand {
196                program: "sudo".to_string(),
197                args: vec![
198                    "apt".to_string(),
199                    "install".to_string(),
200                    "-y".to_string(),
201                    format!("./{deb_file}"),
202                ],
203                env: vec![],
204            },
205            // Update
206            ShellCommand {
207                program: "sudo".to_string(),
208                args: vec!["apt".to_string(), "update".to_string()],
209                env: vec![],
210            },
211            // Linux headers
212            ShellCommand {
213                program: "sudo".to_string(),
214                args: vec![
215                    "apt".to_string(),
216                    "install".to_string(),
217                    "-y".to_string(),
218                    "linux-headers-$(uname -r)".to_string(),
219                    "linux-modules-extra-$(uname -r)".to_string(),
220                ],
221                env: vec![],
222            },
223            // AMDGPU DKMS
224            ShellCommand {
225                program: "sudo".to_string(),
226                args: vec![
227                    "apt".to_string(),
228                    "install".to_string(),
229                    "-y".to_string(),
230                    "amdgpu-dkms".to_string(),
231                ],
232                env: vec![],
233            },
234        ]
235    }
236
237    /// Construct the verification command.
238    pub fn build_verify_command(&self) -> ShellCommand {
239        ShellCommand {
240            program: "lsmod".to_string(),
241            args: vec![],
242            env: vec![],
243        }
244    }
245}
246
247// ===========================================================================
248// Tests
249// ===========================================================================
250
251#[cfg(test)]
252mod tests {
253    use super::*;
254    use crate::platform::detection::{DistroFamily, DistroInfo, PackageManager};
255
256    /// Helper to create a DistroFacade for testing.
257    fn make_distro(family: DistroFamily) -> DistroFacade {
258        DistroFacade::from_info(DistroInfo {
259            id: match family {
260                DistroFamily::Debian => "ubuntu",
261                DistroFamily::Rhel => "fedora",
262                DistroFamily::Arch => "arch",
263                DistroFamily::Suse => "opensuse",
264                DistroFamily::Unknown => "unknown",
265            }
266            .to_string(),
267            name: "Test Distro".to_string(),
268            version: "1.0".to_string(),
269            codename: "test".to_string(),
270            id_like: String::new(),
271            family,
272            pkg_manager: match family {
273                DistroFamily::Debian => PackageManager::Apt,
274                DistroFamily::Rhel => PackageManager::Dnf,
275                DistroFamily::Arch => PackageManager::Pacman,
276                DistroFamily::Suse => PackageManager::Zypper,
277                DistroFamily::Unknown => PackageManager::Apt,
278            },
279        })
280    }
281
282    // -----------------------------------------------------------------------
283    // VAL-INSTALL-029: AMD GPU drivers correct package command
284    // -----------------------------------------------------------------------
285
286    #[test]
287    fn test_package_install_command_debian() {
288        let installer = AmdgpuInstaller::with_defaults();
289        let distro = make_distro(DistroFamily::Debian);
290        let cmd = installer.build_package_install_command(&distro, &["amdgpu-dkms"]);
291
292        assert_eq!(cmd.program, "sudo");
293        assert!(cmd.args.contains(&"apt-get".to_string()));
294        assert!(cmd.args.contains(&"install".to_string()));
295        assert!(cmd.args.contains(&"-y".to_string()));
296        assert!(cmd.args.contains(&"amdgpu-dkms".to_string()));
297    }
298
299    #[test]
300    fn test_package_install_command_rhel() {
301        let installer = AmdgpuInstaller::with_defaults();
302        let distro = make_distro(DistroFamily::Rhel);
303        let cmd = installer.build_package_install_command(&distro, &["amdgpu-dkms"]);
304
305        assert_eq!(cmd.program, "sudo");
306        assert!(cmd.args.contains(&"dnf".to_string()));
307        assert!(cmd.args.contains(&"install".to_string()));
308        assert!(cmd.args.contains(&"-y".to_string()));
309    }
310
311    #[test]
312    fn test_package_install_command_arch() {
313        let installer = AmdgpuInstaller::with_defaults();
314        let distro = make_distro(DistroFamily::Arch);
315        let cmd = installer.build_package_install_command(&distro, &["amdgpu"]);
316
317        assert_eq!(cmd.program, "sudo");
318        assert!(cmd.args.contains(&"pacman".to_string()));
319        assert!(cmd.args.contains(&"-S".to_string()));
320        assert!(cmd.args.contains(&"--noconfirm".to_string()));
321        assert!(cmd.args.contains(&"amdgpu".to_string()));
322    }
323
324    #[test]
325    fn test_package_install_command_suse() {
326        let installer = AmdgpuInstaller::with_defaults();
327        let distro = make_distro(DistroFamily::Suse);
328        let cmd = installer.build_package_install_command(&distro, &["amdgpu-dkms"]);
329
330        assert_eq!(cmd.program, "sudo");
331        assert!(cmd.args.contains(&"zypper".to_string()));
332        assert!(cmd.args.contains(&"install".to_string()));
333        assert!(cmd.args.contains(&"-y".to_string()));
334    }
335
336    #[test]
337    fn test_required_packages_debian() {
338        let installer = AmdgpuInstaller::with_defaults();
339        let distro = make_distro(DistroFamily::Debian);
340        let pkgs = installer.required_packages(&distro);
341        assert!(pkgs.contains(&"amdgpu-dkms"));
342    }
343
344    #[test]
345    fn test_required_packages_arch() {
346        let installer = AmdgpuInstaller::with_defaults();
347        let distro = make_distro(DistroFamily::Arch);
348        let pkgs = installer.required_packages(&distro);
349        assert!(pkgs.contains(&"amdgpu"));
350    }
351
352    #[test]
353    fn test_amdgpu_install_deb_commands() {
354        let installer = AmdgpuInstaller::with_defaults();
355        let cmds = installer.build_amdgpu_install_deb_commands("noble");
356        assert_eq!(cmds.len(), 5);
357
358        // First command should be wget
359        assert_eq!(cmds[0].program, "wget");
360        assert!(cmds[0].args.iter().any(|a| a.contains("repo.radeon.com")));
361        assert!(cmds[0].args.iter().any(|a| a.contains("noble")));
362
363        // Second command should install the deb
364        assert!(cmds[1].args.iter().any(|a| a.contains("amdgpu-install")));
365
366        // Third should be apt update
367        assert!(cmds[2].args.contains(&"update".to_string()));
368
369        // Fourth should install linux headers
370        assert!(cmds[3].args.iter().any(|a| a.contains("linux-headers")));
371
372        // Fifth should install amdgpu-dkms
373        assert!(cmds[4].args.contains(&"amdgpu-dkms".to_string()));
374    }
375
376    #[test]
377    fn test_verify_command() {
378        let installer = AmdgpuInstaller::with_defaults();
379        let cmd = installer.build_verify_command();
380        assert_eq!(cmd.program, "lsmod");
381    }
382}