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)> {
203 let detected: Vec<String> = crate::installer::detect_discrete_gpus();
204 let detected_set: std::collections::HashSet<&str> =
205 detected.iter().map(|s| s.as_str()).collect();
206
207 let gpu_visible = std::env::var("HIP_VISIBLE_DEVICES")
208 .or_else(|_| std::env::var("CUDA_VISIBLE_DEVICES"))
209 .ok()
210 .and_then(|v| {
211 let inherited: Vec<&str> = v
212 .split(',')
213 .map(str::trim)
214 .filter(|s| !s.is_empty())
215 .collect();
216 let all_subset = !inherited.is_empty()
217 && inherited.iter().all(|idx| detected_set.contains(*idx));
218 if all_subset {
219 Some(inherited.join(","))
220 } else {
221 None
222 }
223 })
224 .or_else(|| {
225 if detected.is_empty() {
226 None
227 } else {
228 Some(detected.join(","))
229 }
230 });
231
232 let mut env = vec![
233 ("ROCM_PATH".to_string(), "/opt/rocm".to_string()),
234 ("AMD_LOG_LEVEL".to_string(), "0".to_string()),
235 ];
236 if let Some(visible) = gpu_visible {
237 env.push(("HIP_VISIBLE_DEVICES".to_string(), visible.clone()));
238 env.push(("ROCR_VISIBLE_DEVICES".to_string(), visible));
239 }
240 env
241 }
242
243 pub fn package_name(&self) -> &'static str {
245 PACKAGE_NAME
246 }
247
248 pub fn is_available_on_distro(&self, distro: &DistroFacade) -> bool {
258 !matches!(distro.family(), DistroFamily::Arch)
259 }
260
261 pub fn build_unavailable_message(&self, distro: &DistroFacade) -> Option<String> {
264 if self.is_available_on_distro(distro) {
265 return None;
266 }
267 Some(format!(
268 "MIGraphX Python bindings (pip install migraphx) are not available on {}. \
269 The migraphx pip wheel is not published for Arch-family distros. \
270 If you need Python bindings, consider using the ROCm Docker image \
271 or building MIGraphX from source with Python bindings enabled.",
272 distro.id()
273 ))
274 }
275}
276
277#[cfg(test)]
282mod tests {
283 use super::*;
284
285 #[test]
290 fn test_package_name_matches_original_script() {
291 assert_eq!(
292 PACKAGE_NAME, "migraphx",
293 "Package name must match install_migraphx_python.sh package"
294 );
295 }
296
297 #[test]
298 fn test_install_command_auto_method() {
299 let installer = MigraphxPythonInstaller::new(MigraphxPythonConfig {
300 method: InstallMethod::Auto,
301 ..Default::default()
302 });
303 let cmd = installer.build_install_command();
304 assert_eq!(cmd.program, "python3");
305 assert!(cmd.args.contains(&"-m".to_string()));
306 assert!(cmd.args.contains(&"pip".to_string()));
307 assert!(cmd.args.contains(&"install".to_string()));
308 assert!(cmd.args.contains(&"migraphx".to_string()));
309 assert!(cmd.args.contains(&"--break-system-packages".to_string()));
310 }
311
312 #[test]
313 fn test_install_command_global_method() {
314 let installer = MigraphxPythonInstaller::new(MigraphxPythonConfig {
315 method: InstallMethod::Global,
316 ..Default::default()
317 });
318 let cmd = installer.build_install_command();
319 assert!(cmd.args.contains(&"--break-system-packages".to_string()));
320 }
321
322 #[test]
323 fn test_install_command_venv_method() {
324 let installer = MigraphxPythonInstaller::new(MigraphxPythonConfig {
325 method: InstallMethod::Venv,
326 ..Default::default()
327 });
328 let cmd = installer.build_install_command();
329 assert!(!cmd.args.contains(&"--break-system-packages".to_string()));
331 assert!(cmd.args.contains(&"migraphx".to_string()));
332 }
333
334 #[test]
335 fn test_install_command_with_force() {
336 let installer = MigraphxPythonInstaller::new(MigraphxPythonConfig {
337 force: true,
338 method: InstallMethod::Venv,
339 ..Default::default()
340 });
341 let cmd = installer.build_install_command();
342 assert!(cmd.args.contains(&"--force-reinstall".to_string()));
343 }
344
345 #[test]
346 fn test_install_command_string() {
347 let installer = MigraphxPythonInstaller::with_defaults();
348 let cmd = installer.build_install_command();
349 let s = cmd.to_command_string();
350 assert!(s.contains("python3"));
351 assert!(s.contains("pip install"));
352 assert!(s.contains("migraphx"));
353 }
354
355 #[test]
356 fn test_uv_install_command() {
357 let installer = MigraphxPythonInstaller::with_defaults();
358 let cmd = installer.build_uv_install_command();
359 assert_eq!(cmd.program, "uv");
360 assert!(cmd.args.contains(&"pip".to_string()));
361 assert!(cmd.args.contains(&"install".to_string()));
362 assert!(cmd.args.contains(&"migraphx".to_string()));
363 assert!(cmd.env.iter().any(|(k, _)| k == "ROCM_PATH"));
365 assert!(cmd.env.iter().any(|(k, _)| k == "AMD_LOG_LEVEL"));
366 }
367
368 #[test]
369 fn test_migraphx_check_command() {
370 let installer = MigraphxPythonInstaller::with_defaults();
371 let cmd = installer.build_migraphx_check_command();
372 assert_eq!(cmd.program, "migraphx-driver");
373 assert!(cmd.args.contains(&"--version".to_string()));
374 }
375
376 #[test]
377 fn test_python_import_check() {
378 let installer = MigraphxPythonInstaller::with_defaults();
379 let cmd = installer.build_python_import_check();
380 assert_eq!(cmd.program, "python3");
381 assert!(cmd.args.iter().any(|a| a.contains("import migraphx")));
382 }
383
384 #[test]
385 fn test_rocm_env() {
386 let installer = MigraphxPythonInstaller::with_defaults();
387 let env = installer.build_rocm_env();
388 assert!(env
389 .iter()
390 .any(|(k, v)| k == "ROCM_PATH" && v == "/opt/rocm"));
391 assert!(env.iter().any(|(k, v)| k == "AMD_LOG_LEVEL" && v == "0"));
392 }
393
394 #[test]
397 fn test_is_available_debian() {
398 use crate::platform::detection::{DistroInfo, PackageManager};
399 let installer = MigraphxPythonInstaller::with_defaults();
400 let distro = DistroFacade::from_info(DistroInfo {
401 id: "ubuntu".to_string(),
402 family: DistroFamily::Debian,
403 pkg_manager: PackageManager::Apt,
404 ..Default::default()
405 });
406 assert!(
407 installer.is_available_on_distro(&distro),
408 "MIGraphX Python should be available on Debian"
409 );
410 }
411
412 #[test]
413 fn test_is_available_rhel() {
414 use crate::platform::detection::{DistroInfo, PackageManager};
415 let installer = MigraphxPythonInstaller::with_defaults();
416 let distro = DistroFacade::from_info(DistroInfo {
417 id: "fedora".to_string(),
418 family: DistroFamily::Rhel,
419 pkg_manager: PackageManager::Dnf,
420 ..Default::default()
421 });
422 assert!(
423 installer.is_available_on_distro(&distro),
424 "MIGraphX Python should be available on RHEL"
425 );
426 }
427
428 #[test]
429 fn test_not_available_on_arch() {
430 use crate::platform::detection::{DistroInfo, PackageManager};
431 let installer = MigraphxPythonInstaller::with_defaults();
432 let distro = DistroFacade::from_info(DistroInfo {
433 id: "arch".to_string(),
434 family: DistroFamily::Arch,
435 pkg_manager: PackageManager::Pacman,
436 ..Default::default()
437 });
438 assert!(
439 !installer.is_available_on_distro(&distro),
440 "MIGraphX Python should NOT be available on Arch"
441 );
442 }
443
444 #[test]
445 fn test_not_available_on_cachyos() {
446 use crate::platform::detection::{DistroInfo, PackageManager};
447 let installer = MigraphxPythonInstaller::with_defaults();
448 let distro = DistroFacade::from_info(DistroInfo {
449 id: "cachyos".to_string(),
450 family: DistroFamily::Arch,
451 pkg_manager: PackageManager::Pacman,
452 ..Default::default()
453 });
454 assert!(
455 !installer.is_available_on_distro(&distro),
456 "MIGraphX Python should NOT be available on CachyOS"
457 );
458 }
459
460 #[test]
461 fn test_unavailable_message_debian_is_none() {
462 use crate::platform::detection::{DistroInfo, PackageManager};
463 let installer = MigraphxPythonInstaller::with_defaults();
464 let distro = DistroFacade::from_info(DistroInfo {
465 id: "ubuntu".to_string(),
466 family: DistroFamily::Debian,
467 pkg_manager: PackageManager::Apt,
468 ..Default::default()
469 });
470 assert!(
471 installer.build_unavailable_message(&distro).is_none(),
472 "Debian should not have an unavailable message"
473 );
474 }
475
476 #[test]
477 fn test_unavailable_message_arch_is_informative() {
478 use crate::platform::detection::{DistroInfo, PackageManager};
479 let installer = MigraphxPythonInstaller::with_defaults();
480 let distro = DistroFacade::from_info(DistroInfo {
481 id: "arch".to_string(),
482 family: DistroFamily::Arch,
483 pkg_manager: PackageManager::Pacman,
484 ..Default::default()
485 });
486 let msg = installer
487 .build_unavailable_message(&distro)
488 .expect("Arch should have an unavailable message");
489 assert!(
490 msg.contains("not available"),
491 "Message should say not available: {msg}"
492 );
493 assert!(msg.contains("Arch"), "Message should mention Arch: {msg}");
494 assert!(msg.contains("pip"), "Message should mention pip: {msg}");
495 assert!(
496 msg.contains("Docker") || msg.contains("source"),
497 "Message should suggest alternatives: {msg}"
498 );
499 }
500}