rusty_stack/installers/components/
migraphx_python.rs1use crate::installers::common::DistroFacade;
18use crate::platform::detection::DistroFamily;
19
20#[derive(Debug, Clone, Copy, PartialEq, Eq)]
26pub enum InstallMethod {
27 Global,
29 Venv,
31 Auto,
33}
34
35impl std::fmt::Display for InstallMethod {
36 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
37 match self {
38 InstallMethod::Global => write!(f, "global"),
39 InstallMethod::Venv => write!(f, "venv"),
40 InstallMethod::Auto => write!(f, "auto"),
41 }
42 }
43}
44
45#[derive(Debug, Clone, PartialEq, Eq)]
47pub struct ShellCommand {
48 pub program: String,
50 pub args: Vec<String>,
52 pub env: Vec<(String, String)>,
54}
55
56impl ShellCommand {
57 pub fn to_command_string(&self) -> String {
59 let env_prefix = self
60 .env
61 .iter()
62 .map(|(k, v)| format!("{k}={v}"))
63 .collect::<Vec<_>>()
64 .join(" ");
65 let cmd = if self.args.is_empty() {
66 self.program.clone()
67 } else {
68 format!("{} {}", self.program, self.args.join(" "))
69 };
70 if env_prefix.is_empty() {
71 cmd
72 } else {
73 format!("{env_prefix} {cmd}")
74 }
75 }
76}
77
78#[derive(Debug, Clone)]
80pub struct MigraphxPythonConfig {
81 pub python_bin: String,
83 pub method: InstallMethod,
85 pub dry_run: bool,
87 pub force: bool,
89}
90
91impl Default for MigraphxPythonConfig {
92 fn default() -> Self {
93 Self {
94 python_bin: "python3".to_string(),
95 method: InstallMethod::Auto,
96 dry_run: false,
97 force: false,
98 }
99 }
100}
101
102pub const PACKAGE_NAME: &str = "migraphx";
104
105pub struct MigraphxPythonInstaller {
107 config: MigraphxPythonConfig,
108}
109
110impl MigraphxPythonInstaller {
111 pub fn new(config: MigraphxPythonConfig) -> Self {
113 Self { config }
114 }
115
116 pub fn with_defaults() -> Self {
118 Self::new(MigraphxPythonConfig::default())
119 }
120
121 pub fn build_install_command(&self) -> ShellCommand {
131 let use_break = self.config.method == InstallMethod::Global
132 || self.config.method == InstallMethod::Auto;
133
134 let mut args = vec!["-m".to_string(), "pip".to_string(), "install".to_string()];
135 if use_break {
136 args.push("--break-system-packages".to_string());
137 }
138 if self.config.force {
139 args.push("--force-reinstall".to_string());
140 }
141 args.push(PACKAGE_NAME.to_string());
142
143 ShellCommand {
144 program: self.config.python_bin.clone(),
145 args,
146 env: vec![],
147 }
148 }
149
150 pub fn build_uv_install_command(&self) -> ShellCommand {
152 let mut args = vec!["pip".to_string(), "install".to_string()];
153 if self.config.force {
154 args.push("--force-reinstall".to_string());
155 }
156 args.push(PACKAGE_NAME.to_string());
157
158 ShellCommand {
159 program: "uv".to_string(),
160 args,
161 env: vec![
162 ("ROCM_PATH".to_string(), "/opt/rocm".to_string()),
163 ("AMD_LOG_LEVEL".to_string(), "0".to_string()),
164 ],
165 }
166 }
167
168 pub fn build_migraphx_check_command(&self) -> ShellCommand {
170 ShellCommand {
171 program: "migraphx-driver".to_string(),
172 args: vec!["--version".to_string()],
173 env: vec![],
174 }
175 }
176
177 pub fn build_python_import_check(&self) -> ShellCommand {
179 ShellCommand {
180 program: self.config.python_bin.clone(),
181 args: vec![
182 "-c".to_string(),
183 "import migraphx; print(getattr(migraphx, '__version__', 'unknown'))".to_string(),
184 ],
185 env: vec![],
186 }
187 }
188
189 pub fn build_rocm_env(&self) -> Vec<(String, String)> {
191 let gpu_visible = std::env::var("HIP_VISIBLE_DEVICES")
192 .or_else(|_| std::env::var("CUDA_VISIBLE_DEVICES"))
193 .unwrap_or_else(|_| "0".to_string());
194
195 vec![
196 ("ROCM_PATH".to_string(), "/opt/rocm".to_string()),
197 ("AMD_LOG_LEVEL".to_string(), "0".to_string()),
198 ("HIP_VISIBLE_DEVICES".to_string(), gpu_visible.clone()),
199 ("ROCR_VISIBLE_DEVICES".to_string(), gpu_visible),
200 ]
201 }
202
203 pub fn package_name(&self) -> &'static str {
205 PACKAGE_NAME
206 }
207
208 pub fn is_available_on_distro(&self, distro: &DistroFacade) -> bool {
218 !matches!(distro.family(), DistroFamily::Arch)
219 }
220
221 pub fn build_unavailable_message(&self, distro: &DistroFacade) -> Option<String> {
224 if self.is_available_on_distro(distro) {
225 return None;
226 }
227 Some(format!(
228 "MIGraphX Python bindings (pip install migraphx) are not available on {}. \
229 The migraphx pip wheel is not published for Arch-family distros. \
230 If you need Python bindings, consider using the ROCm Docker image \
231 or building MIGraphX from source with Python bindings enabled.",
232 distro.id()
233 ))
234 }
235}
236
237#[cfg(test)]
242mod tests {
243 use super::*;
244
245 #[test]
250 fn test_package_name_matches_original_script() {
251 assert_eq!(
252 PACKAGE_NAME, "migraphx",
253 "Package name must match install_migraphx_python.sh package"
254 );
255 }
256
257 #[test]
258 fn test_install_command_auto_method() {
259 let installer = MigraphxPythonInstaller::new(MigraphxPythonConfig {
260 method: InstallMethod::Auto,
261 ..Default::default()
262 });
263 let cmd = installer.build_install_command();
264 assert_eq!(cmd.program, "python3");
265 assert!(cmd.args.contains(&"-m".to_string()));
266 assert!(cmd.args.contains(&"pip".to_string()));
267 assert!(cmd.args.contains(&"install".to_string()));
268 assert!(cmd.args.contains(&"migraphx".to_string()));
269 assert!(cmd.args.contains(&"--break-system-packages".to_string()));
270 }
271
272 #[test]
273 fn test_install_command_global_method() {
274 let installer = MigraphxPythonInstaller::new(MigraphxPythonConfig {
275 method: InstallMethod::Global,
276 ..Default::default()
277 });
278 let cmd = installer.build_install_command();
279 assert!(cmd.args.contains(&"--break-system-packages".to_string()));
280 }
281
282 #[test]
283 fn test_install_command_venv_method() {
284 let installer = MigraphxPythonInstaller::new(MigraphxPythonConfig {
285 method: InstallMethod::Venv,
286 ..Default::default()
287 });
288 let cmd = installer.build_install_command();
289 assert!(!cmd.args.contains(&"--break-system-packages".to_string()));
291 assert!(cmd.args.contains(&"migraphx".to_string()));
292 }
293
294 #[test]
295 fn test_install_command_with_force() {
296 let installer = MigraphxPythonInstaller::new(MigraphxPythonConfig {
297 force: true,
298 method: InstallMethod::Venv,
299 ..Default::default()
300 });
301 let cmd = installer.build_install_command();
302 assert!(cmd.args.contains(&"--force-reinstall".to_string()));
303 }
304
305 #[test]
306 fn test_install_command_string() {
307 let installer = MigraphxPythonInstaller::with_defaults();
308 let cmd = installer.build_install_command();
309 let s = cmd.to_command_string();
310 assert!(s.contains("python3"));
311 assert!(s.contains("pip install"));
312 assert!(s.contains("migraphx"));
313 }
314
315 #[test]
316 fn test_uv_install_command() {
317 let installer = MigraphxPythonInstaller::with_defaults();
318 let cmd = installer.build_uv_install_command();
319 assert_eq!(cmd.program, "uv");
320 assert!(cmd.args.contains(&"pip".to_string()));
321 assert!(cmd.args.contains(&"install".to_string()));
322 assert!(cmd.args.contains(&"migraphx".to_string()));
323 assert!(cmd.env.iter().any(|(k, _)| k == "ROCM_PATH"));
325 assert!(cmd.env.iter().any(|(k, _)| k == "AMD_LOG_LEVEL"));
326 }
327
328 #[test]
329 fn test_migraphx_check_command() {
330 let installer = MigraphxPythonInstaller::with_defaults();
331 let cmd = installer.build_migraphx_check_command();
332 assert_eq!(cmd.program, "migraphx-driver");
333 assert!(cmd.args.contains(&"--version".to_string()));
334 }
335
336 #[test]
337 fn test_python_import_check() {
338 let installer = MigraphxPythonInstaller::with_defaults();
339 let cmd = installer.build_python_import_check();
340 assert_eq!(cmd.program, "python3");
341 assert!(cmd.args.iter().any(|a| a.contains("import migraphx")));
342 }
343
344 #[test]
345 fn test_rocm_env() {
346 let installer = MigraphxPythonInstaller::with_defaults();
347 let env = installer.build_rocm_env();
348 assert!(env
349 .iter()
350 .any(|(k, v)| k == "ROCM_PATH" && v == "/opt/rocm"));
351 assert!(env.iter().any(|(k, v)| k == "AMD_LOG_LEVEL" && v == "0"));
352 }
353
354 #[test]
357 fn test_is_available_debian() {
358 use crate::platform::detection::{DistroInfo, PackageManager};
359 let installer = MigraphxPythonInstaller::with_defaults();
360 let distro = DistroFacade::from_info(DistroInfo {
361 id: "ubuntu".to_string(),
362 family: DistroFamily::Debian,
363 pkg_manager: PackageManager::Apt,
364 ..Default::default()
365 });
366 assert!(
367 installer.is_available_on_distro(&distro),
368 "MIGraphX Python should be available on Debian"
369 );
370 }
371
372 #[test]
373 fn test_is_available_rhel() {
374 use crate::platform::detection::{DistroInfo, PackageManager};
375 let installer = MigraphxPythonInstaller::with_defaults();
376 let distro = DistroFacade::from_info(DistroInfo {
377 id: "fedora".to_string(),
378 family: DistroFamily::Rhel,
379 pkg_manager: PackageManager::Dnf,
380 ..Default::default()
381 });
382 assert!(
383 installer.is_available_on_distro(&distro),
384 "MIGraphX Python should be available on RHEL"
385 );
386 }
387
388 #[test]
389 fn test_not_available_on_arch() {
390 use crate::platform::detection::{DistroInfo, PackageManager};
391 let installer = MigraphxPythonInstaller::with_defaults();
392 let distro = DistroFacade::from_info(DistroInfo {
393 id: "arch".to_string(),
394 family: DistroFamily::Arch,
395 pkg_manager: PackageManager::Pacman,
396 ..Default::default()
397 });
398 assert!(
399 !installer.is_available_on_distro(&distro),
400 "MIGraphX Python should NOT be available on Arch"
401 );
402 }
403
404 #[test]
405 fn test_not_available_on_cachyos() {
406 use crate::platform::detection::{DistroInfo, PackageManager};
407 let installer = MigraphxPythonInstaller::with_defaults();
408 let distro = DistroFacade::from_info(DistroInfo {
409 id: "cachyos".to_string(),
410 family: DistroFamily::Arch,
411 pkg_manager: PackageManager::Pacman,
412 ..Default::default()
413 });
414 assert!(
415 !installer.is_available_on_distro(&distro),
416 "MIGraphX Python should NOT be available on CachyOS"
417 );
418 }
419
420 #[test]
421 fn test_unavailable_message_debian_is_none() {
422 use crate::platform::detection::{DistroInfo, PackageManager};
423 let installer = MigraphxPythonInstaller::with_defaults();
424 let distro = DistroFacade::from_info(DistroInfo {
425 id: "ubuntu".to_string(),
426 family: DistroFamily::Debian,
427 pkg_manager: PackageManager::Apt,
428 ..Default::default()
429 });
430 assert!(
431 installer.build_unavailable_message(&distro).is_none(),
432 "Debian should not have an unavailable message"
433 );
434 }
435
436 #[test]
437 fn test_unavailable_message_arch_is_informative() {
438 use crate::platform::detection::{DistroInfo, PackageManager};
439 let installer = MigraphxPythonInstaller::with_defaults();
440 let distro = DistroFacade::from_info(DistroInfo {
441 id: "arch".to_string(),
442 family: DistroFamily::Arch,
443 pkg_manager: PackageManager::Pacman,
444 ..Default::default()
445 });
446 let msg = installer
447 .build_unavailable_message(&distro)
448 .expect("Arch should have an unavailable message");
449 assert!(
450 msg.contains("not available"),
451 "Message should say not available: {msg}"
452 );
453 assert!(msg.contains("Arch"), "Message should mention Arch: {msg}");
454 assert!(msg.contains("pip"), "Message should mention pip: {msg}");
455 assert!(
456 msg.contains("Docker") || msg.contains("source"),
457 "Message should suggest alternatives: {msg}"
458 );
459 }
460}