1use 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 const MIGRAPHX_BUILD_PYTHON_ENV: &str = "MLSTACK_MIGRAPHX_BUILD_PYTHON";
110
111pub const MIGRAPHX_SOURCE_BRANCH_ENV: &str = "MLSTACK_MIGRAPHX_SOURCE_BRANCH";
115
116pub const AMDMIGRAPHX_DEFAULT_BRANCH: &str = "rocm-7.2.3";
118
119pub const AMDMIGRAPHX_REPO: &str = "https://github.com/ROCm/AMDMIGraphX";
121
122pub fn build_python_source_requested() -> bool {
124 let v = std::env::var(MIGRAPHX_BUILD_PYTHON_ENV)
125 .unwrap_or_default()
126 .trim()
127 .to_ascii_lowercase();
128 matches!(v.as_str(), "1" | "true" | "yes" | "on")
129}
130
131pub fn source_branch() -> String {
134 let v = std::env::var(MIGRAPHX_SOURCE_BRANCH_ENV)
135 .unwrap_or_default()
136 .trim()
137 .to_string();
138 if v.is_empty() {
139 AMDMIGRAPHX_DEFAULT_BRANCH.to_string()
140 } else {
141 v
142 }
143}
144
145pub struct MigraphxPythonInstaller {
147 config: MigraphxPythonConfig,
148}
149
150impl MigraphxPythonInstaller {
151 pub fn new(config: MigraphxPythonConfig) -> Self {
153 Self { config }
154 }
155
156 pub fn with_defaults() -> Self {
158 Self::new(MigraphxPythonConfig::default())
159 }
160
161 pub fn build_install_command(&self) -> ShellCommand {
171 let use_break = self.config.method == InstallMethod::Global
172 || self.config.method == InstallMethod::Auto;
173
174 let mut args = vec!["-m".to_string(), "pip".to_string(), "install".to_string()];
175 if use_break {
176 args.push("--break-system-packages".to_string());
177 }
178 if self.config.force {
179 args.push("--force-reinstall".to_string());
180 }
181 args.push(PACKAGE_NAME.to_string());
182
183 ShellCommand {
184 program: self.config.python_bin.clone(),
185 args,
186 env: vec![],
187 }
188 }
189
190 pub fn build_uv_install_command(&self) -> ShellCommand {
192 let mut args = vec!["pip".to_string(), "install".to_string()];
193 if self.config.force {
194 args.push("--force-reinstall".to_string());
195 }
196 args.push(PACKAGE_NAME.to_string());
197
198 ShellCommand {
199 program: "uv".to_string(),
200 args,
201 env: vec![
202 ("ROCM_PATH".to_string(), "/opt/rocm".to_string()),
203 ("AMD_LOG_LEVEL".to_string(), "0".to_string()),
204 ],
205 }
206 }
207
208 pub fn build_source_commands(
226 &self,
227 workdir: &str,
228 rocm_prefix: &str,
229 gpu_arch: &str,
230 branch: &str,
231 ) -> Vec<ShellCommand> {
232 let src = format!("{workdir}/AMDMIGraphX");
233 let build = format!("{src}/build");
234 let clang = format!("{rocm_prefix}/llvm/bin/clang");
235 let clangxx = format!("{rocm_prefix}/llvm/bin/clang++");
236 vec![
237 ShellCommand {
239 program: "git".to_string(),
240 args: vec![
241 "clone".to_string(),
242 "--depth".to_string(),
243 "1".to_string(),
244 "--single-branch".to_string(),
245 "--branch".to_string(),
246 branch.to_string(),
247 AMDMIGRAPHX_REPO.to_string(),
248 src.clone(),
249 ],
250 env: vec![],
251 },
252 ShellCommand {
254 program: "cmake".to_string(),
255 args: vec![
256 "-S".to_string(),
257 src.clone(),
258 "-B".to_string(),
259 build.clone(),
260 "-DMIGRAPHX_ENABLE_PYTHON=On".to_string(),
261 format!("-DCMAKE_PREFIX_PATH={rocm_prefix}"),
262 format!("-DGPU_TARGETS={gpu_arch}"),
263 format!("-DCMAKE_C_COMPILER={clang}"),
264 format!("-DCMAKE_CXX_COMPILER={clangxx}"),
265 "-DCMAKE_BUILD_TYPE=Release".to_string(),
266 ],
267 env: vec![],
268 },
269 ShellCommand {
271 program: "cmake".to_string(),
272 args: vec![
273 "--build".to_string(),
274 build,
275 "--target".to_string(),
276 "migraphx_py".to_string(),
277 "-j".to_string(),
278 ],
279 env: vec![],
280 },
281 ShellCommand {
283 program: self.config.python_bin.clone(),
284 args: vec![
285 "-m".to_string(),
286 "pip".to_string(),
287 "install".to_string(),
288 format!("{src}/python"),
289 ],
290 env: vec![],
291 },
292 ]
293 }
294
295 pub fn build_migraphx_check_command(&self) -> ShellCommand {
297 ShellCommand {
298 program: "migraphx-driver".to_string(),
299 args: vec!["--version".to_string()],
300 env: vec![],
301 }
302 }
303
304 pub fn build_python_import_check(&self) -> ShellCommand {
306 ShellCommand {
307 program: self.config.python_bin.clone(),
308 args: vec![
309 "-c".to_string(),
310 "import migraphx; print(getattr(migraphx, '__version__', 'unknown'))".to_string(),
311 ],
312 env: vec![],
313 }
314 }
315
316 pub fn package_name(&self) -> &'static str {
318 PACKAGE_NAME
319 }
320
321 pub fn is_available_on_distro(&self, distro: &DistroFacade) -> bool {
331 !matches!(distro.family(), DistroFamily::Arch)
332 }
333
334 pub fn build_unavailable_message(&self, distro: &DistroFacade) -> Option<String> {
337 if self.is_available_on_distro(distro) {
338 return None;
339 }
340 Some(format!(
341 "MIGraphX Python bindings (pip install migraphx) are not available on {}. \
342 The migraphx pip wheel is not published for Arch-family distros. \
343 If you need Python bindings, consider using the ROCm Docker image \
344 or building MIGraphX from source with Python bindings enabled.",
345 distro.id()
346 ))
347 }
348}
349
350#[cfg(test)]
355mod tests {
356 use super::*;
357
358 #[test]
363 fn test_package_name_matches_original_script() {
364 assert_eq!(
365 PACKAGE_NAME, "migraphx",
366 "Package name must match install_migraphx_python.sh package"
367 );
368 }
369
370 #[test]
371 fn test_install_command_auto_method() {
372 let installer = MigraphxPythonInstaller::new(MigraphxPythonConfig {
373 method: InstallMethod::Auto,
374 ..Default::default()
375 });
376 let cmd = installer.build_install_command();
377 assert_eq!(cmd.program, "python3");
378 assert!(cmd.args.contains(&"-m".to_string()));
379 assert!(cmd.args.contains(&"pip".to_string()));
380 assert!(cmd.args.contains(&"install".to_string()));
381 assert!(cmd.args.contains(&"migraphx".to_string()));
382 assert!(cmd.args.contains(&"--break-system-packages".to_string()));
383 }
384
385 #[test]
386 fn test_install_command_global_method() {
387 let installer = MigraphxPythonInstaller::new(MigraphxPythonConfig {
388 method: InstallMethod::Global,
389 ..Default::default()
390 });
391 let cmd = installer.build_install_command();
392 assert!(cmd.args.contains(&"--break-system-packages".to_string()));
393 }
394
395 #[test]
396 fn test_install_command_venv_method() {
397 let installer = MigraphxPythonInstaller::new(MigraphxPythonConfig {
398 method: InstallMethod::Venv,
399 ..Default::default()
400 });
401 let cmd = installer.build_install_command();
402 assert!(!cmd.args.contains(&"--break-system-packages".to_string()));
404 assert!(cmd.args.contains(&"migraphx".to_string()));
405 }
406
407 #[test]
408 fn test_install_command_with_force() {
409 let installer = MigraphxPythonInstaller::new(MigraphxPythonConfig {
410 force: true,
411 method: InstallMethod::Venv,
412 ..Default::default()
413 });
414 let cmd = installer.build_install_command();
415 assert!(cmd.args.contains(&"--force-reinstall".to_string()));
416 }
417
418 #[test]
419 fn test_install_command_string() {
420 let installer = MigraphxPythonInstaller::with_defaults();
421 let cmd = installer.build_install_command();
422 let s = cmd.to_command_string();
423 assert!(s.contains("python3"));
424 assert!(s.contains("pip install"));
425 assert!(s.contains("migraphx"));
426 }
427
428 #[test]
429 fn test_uv_install_command() {
430 let installer = MigraphxPythonInstaller::with_defaults();
431 let cmd = installer.build_uv_install_command();
432 assert_eq!(cmd.program, "uv");
433 assert!(cmd.args.contains(&"pip".to_string()));
434 assert!(cmd.args.contains(&"install".to_string()));
435 assert!(cmd.args.contains(&"migraphx".to_string()));
436 assert!(cmd.env.iter().any(|(k, _)| k == "ROCM_PATH"));
438 assert!(cmd.env.iter().any(|(k, _)| k == "AMD_LOG_LEVEL"));
439 }
440
441 #[test]
442 fn test_migraphx_check_command() {
443 let installer = MigraphxPythonInstaller::with_defaults();
444 let cmd = installer.build_migraphx_check_command();
445 assert_eq!(cmd.program, "migraphx-driver");
446 assert!(cmd.args.contains(&"--version".to_string()));
447 }
448
449 #[test]
450 fn test_python_import_check() {
451 let installer = MigraphxPythonInstaller::with_defaults();
452 let cmd = installer.build_python_import_check();
453 assert_eq!(cmd.program, "python3");
454 assert!(cmd.args.iter().any(|a| a.contains("import migraphx")));
455 }
456
457 #[test]
460 fn test_is_available_debian() {
461 use crate::platform::detection::{DistroInfo, PackageManager};
462 let installer = MigraphxPythonInstaller::with_defaults();
463 let distro = DistroFacade::from_info(DistroInfo {
464 id: "ubuntu".to_string(),
465 family: DistroFamily::Debian,
466 pkg_manager: PackageManager::Apt,
467 ..Default::default()
468 });
469 assert!(
470 installer.is_available_on_distro(&distro),
471 "MIGraphX Python should be available on Debian"
472 );
473 }
474
475 #[test]
476 fn test_is_available_rhel() {
477 use crate::platform::detection::{DistroInfo, PackageManager};
478 let installer = MigraphxPythonInstaller::with_defaults();
479 let distro = DistroFacade::from_info(DistroInfo {
480 id: "fedora".to_string(),
481 family: DistroFamily::Rhel,
482 pkg_manager: PackageManager::Dnf,
483 ..Default::default()
484 });
485 assert!(
486 installer.is_available_on_distro(&distro),
487 "MIGraphX Python should be available on RHEL"
488 );
489 }
490
491 #[test]
492 fn test_not_available_on_arch() {
493 use crate::platform::detection::{DistroInfo, PackageManager};
494 let installer = MigraphxPythonInstaller::with_defaults();
495 let distro = DistroFacade::from_info(DistroInfo {
496 id: "arch".to_string(),
497 family: DistroFamily::Arch,
498 pkg_manager: PackageManager::Pacman,
499 ..Default::default()
500 });
501 assert!(
502 !installer.is_available_on_distro(&distro),
503 "MIGraphX Python should NOT be available on Arch"
504 );
505 }
506
507 #[test]
508 fn test_not_available_on_cachyos() {
509 use crate::platform::detection::{DistroInfo, PackageManager};
510 let installer = MigraphxPythonInstaller::with_defaults();
511 let distro = DistroFacade::from_info(DistroInfo {
512 id: "cachyos".to_string(),
513 family: DistroFamily::Arch,
514 pkg_manager: PackageManager::Pacman,
515 ..Default::default()
516 });
517 assert!(
518 !installer.is_available_on_distro(&distro),
519 "MIGraphX Python should NOT be available on CachyOS"
520 );
521 }
522
523 #[test]
524 fn test_unavailable_message_debian_is_none() {
525 use crate::platform::detection::{DistroInfo, PackageManager};
526 let installer = MigraphxPythonInstaller::with_defaults();
527 let distro = DistroFacade::from_info(DistroInfo {
528 id: "ubuntu".to_string(),
529 family: DistroFamily::Debian,
530 pkg_manager: PackageManager::Apt,
531 ..Default::default()
532 });
533 assert!(
534 installer.build_unavailable_message(&distro).is_none(),
535 "Debian should not have an unavailable message"
536 );
537 }
538
539 #[test]
540 fn test_unavailable_message_arch_is_informative() {
541 use crate::platform::detection::{DistroInfo, PackageManager};
542 let installer = MigraphxPythonInstaller::with_defaults();
543 let distro = DistroFacade::from_info(DistroInfo {
544 id: "arch".to_string(),
545 family: DistroFamily::Arch,
546 pkg_manager: PackageManager::Pacman,
547 ..Default::default()
548 });
549 let msg = installer
550 .build_unavailable_message(&distro)
551 .expect("Arch should have an unavailable message");
552 assert!(
553 msg.contains("not available"),
554 "Message should say not available: {msg}"
555 );
556 assert!(msg.contains("Arch"), "Message should mention Arch: {msg}");
557 assert!(msg.contains("pip"), "Message should mention pip: {msg}");
558 assert!(
559 msg.contains("Docker") || msg.contains("source"),
560 "Message should suggest alternatives: {msg}"
561 );
562 }
563
564 #[test]
567 fn test_build_python_source_requested_env_gated() {
568 let _env = crate::test_support::lock_env();
569 std::env::remove_var(MIGRAPHX_BUILD_PYTHON_ENV);
570 assert!(!build_python_source_requested(), "default must be OFF");
571 for v in ["1", "true", "TRUE", "yes", "on"] {
572 std::env::set_var(MIGRAPHX_BUILD_PYTHON_ENV, v);
573 assert!(build_python_source_requested(), "should be ON for {v:?}");
574 }
575 std::env::set_var(MIGRAPHX_BUILD_PYTHON_ENV, "0");
576 assert!(!build_python_source_requested());
577 }
578
579 #[test]
580 fn test_source_branch_default_and_override() {
581 let _env = crate::test_support::lock_env();
582 std::env::remove_var(MIGRAPHX_SOURCE_BRANCH_ENV);
583 assert_eq!(source_branch(), AMDMIGRAPHX_DEFAULT_BRANCH);
584 std::env::set_var(MIGRAPHX_SOURCE_BRANCH_ENV, "rocm-7.2.4");
585 assert_eq!(source_branch(), "rocm-7.2.4");
586 }
587
588 #[test]
589 fn test_build_source_commands_sequence() {
590 let inst = MigraphxPythonInstaller::new(MigraphxPythonConfig {
591 python_bin: "/venv/bin/python".to_string(),
592 ..Default::default()
593 });
594 let cmds = inst.build_source_commands("/tmp/wd", "/opt/rocm", "gfx1100", "rocm-7.2.3");
595 assert_eq!(
596 cmds.len(),
597 4,
598 "expected: clone, configure, build, pip install"
599 );
600
601 assert_eq!(cmds[0].program, "git");
603 assert!(cmds[0].args.contains(&"clone".to_string()));
604 assert!(cmds[0].args.contains(&"--branch".to_string()));
605 assert!(cmds[0].args.contains(&"rocm-7.2.3".to_string()));
606 assert!(cmds[0].args.contains(&AMDMIGRAPHX_REPO.to_string()));
607
608 assert_eq!(cmds[1].program, "cmake");
610 assert!(cmds[1]
611 .args
612 .contains(&"-DMIGRAPHX_ENABLE_PYTHON=On".to_string()));
613 assert!(cmds[1]
614 .args
615 .contains(&"-DCMAKE_PREFIX_PATH=/opt/rocm".to_string()));
616 assert!(cmds[1].args.contains(&"-DGPU_TARGETS=gfx1100".to_string()));
617 assert!(cmds[1]
618 .args
619 .contains(&"-DCMAKE_CXX_COMPILER=/opt/rocm/llvm/bin/clang++".to_string()));
620
621 assert_eq!(cmds[2].program, "cmake");
623 assert!(cmds[2].args.contains(&"--target".to_string()));
624 assert!(cmds[2].args.contains(&"migraphx_py".to_string()));
625
626 assert_eq!(cmds[3].program, "/venv/bin/python");
628 assert!(cmds[3].args.contains(&"install".to_string()));
629 assert!(cmds[3]
630 .args
631 .iter()
632 .any(|a| a.ends_with("/AMDMIGraphX/python")));
633 }
634}