Skip to main content

rusty_stack/installers/components/
onnxruntime.rs

1//! ONNX Runtime installer — ports `scripts/build_onnxruntime_multi.sh`.
2//!
3//! Constructs correct cmake commands with ROCm integration and Python bindings,
4//! builds from source using ONNX Runtime's build.sh, and pip installs from
5//! the built wheel. Falls back to prebuilt wheel if available.
6//!
7//! ONNX Runtime depends on ROCm.
8//!
9//! # Validation Assertions
10//!
11//! - **VAL-INSTALL-010**: ONNX Runtime correct cmake command
12//! - **VAL-INSTALL-011**: ONNX Runtime pip install from build output
13//! - **VAL-INSTALL-014**: Source builds parse build output for errors
14//! - **VAL-INSTALL-045**: ONNX Runtime declares dependency on ROCm
15
16use crate::installers::common::RocmEnv;
17use std::path::PathBuf;
18
19// ===========================================================================
20// Types
21// ===========================================================================
22
23/// GPU architecture specification for HIP builds.
24#[derive(Debug, Clone, PartialEq, Eq)]
25pub enum HipArchs {
26    /// gfx1030 family.
27    Gfx1030,
28    /// gfx1100 family (gfx1100-gfx1103).
29    Gfx1100,
30    /// gfx1200 family (gfx1200-gfx1201).
31    Gfx1200,
32    /// Default fallback.
33    Default,
34}
35
36impl HipArchs {
37    /// Detect HIP architectures from a GPU arch string.
38    pub fn from_gpu_arch(gpu_arch: &str) -> Self {
39        if gpu_arch.starts_with("gfx103") {
40            HipArchs::Gfx1030
41        } else if gpu_arch.starts_with("gfx110") {
42            HipArchs::Gfx1100
43        } else if gpu_arch.starts_with("gfx120") {
44            HipArchs::Gfx1200
45        } else {
46            HipArchs::Default
47        }
48    }
49
50    /// Get the CMAKE_HIP_ARCHITECTURES string.
51    pub fn cmake_hip_architectures(&self) -> &str {
52        match self {
53            HipArchs::Gfx1030 => "gfx1030",
54            HipArchs::Gfx1100 => "gfx1100;gfx1101;gfx1102;gfx1103",
55            HipArchs::Gfx1200 => "gfx1200;gfx1201",
56            HipArchs::Default => "gfx1100",
57        }
58    }
59}
60
61/// ONNX Runtime install method.
62#[derive(Debug, Clone, PartialEq, Eq)]
63pub enum OnnxInstallMethod {
64    /// Install onnxruntime-migraphx from AMD manylinux repo (default).
65    MigraphxWheel,
66    /// Build from source with ROCm + MIGraphX EP (includes ROCMExecutionProvider).
67    SourceBuild,
68    /// Install prebuilt onnxruntime-rocm from PyPI (legacy, may be ABI-incompatible).
69    PrebuiltWheel,
70}
71
72impl Default for OnnxInstallMethod {
73    fn default() -> Self {
74        OnnxInstallMethod::MigraphxWheel
75    }
76}
77
78/// Configuration for the ONNX Runtime installer.
79#[derive(Debug, Clone)]
80pub struct OnnxRuntimeConfig {
81    /// Python binary to use.
82    pub python_bin: String,
83    /// Whether to run in dry-run mode.
84    pub dry_run: bool,
85    /// ROCm version string (e.g., "7.2.0").
86    pub rocm_version: Option<String>,
87    /// ROCm release for AMD repo URL (e.g., "7.2.4").
88    pub rocm_release: Option<String>,
89    /// GPU architecture string (e.g., "gfx1100").
90    pub gpu_arch: Option<String>,
91    /// Working directory for the build (defaults to /tmp/onnxruntime-rocm).
92    pub workdir: Option<PathBuf>,
93    /// Preinstalled Eigen path.
94    pub eigen_path: Option<PathBuf>,
95    /// Whether to use preinstalled Eigen.
96    pub use_preinstalled_eigen: bool,
97    /// Install method (default: MIGraphX wheel from AMD repo).
98    pub install_method: OnnxInstallMethod,
99}
100
101impl Default for OnnxRuntimeConfig {
102    fn default() -> Self {
103        Self {
104            python_bin: "python3".to_string(),
105            dry_run: false,
106            rocm_version: None,
107            rocm_release: None,
108            gpu_arch: None,
109            workdir: None,
110            eigen_path: None,
111            use_preinstalled_eigen: false,
112            install_method: OnnxInstallMethod::default(),
113        }
114    }
115}
116
117impl OnnxRuntimeConfig {
118    /// Get the working directory for the build.
119    pub fn workdir(&self) -> PathBuf {
120        self.workdir
121            .clone()
122            .unwrap_or_else(|| std::env::temp_dir().join("onnxruntime-rocm"))
123    }
124
125    /// Get the effective GPU arch string.
126    pub fn gpu_arch(&self) -> &str {
127        self.gpu_arch.as_deref().unwrap_or("gfx1100")
128    }
129
130    /// Get the effective ROCm version string.
131    pub fn rocm_version(&self) -> &str {
132        self.rocm_version.as_deref().unwrap_or("7.2")
133    }
134
135    /// Get the effective ROCm release string (for AMD repo URL).
136    pub fn rocm_release(&self) -> &str {
137        self.rocm_release.as_deref().unwrap_or("7.2.4")
138    }
139}
140
141/// A constructed shell command.
142#[derive(Debug, Clone, PartialEq, Eq)]
143pub struct ShellCommand {
144    /// The program to run.
145    pub program: String,
146    /// Arguments to pass.
147    pub args: Vec<String>,
148    /// Environment variables to set.
149    pub env: Vec<(String, String)>,
150    /// Working directory for the command.
151    pub working_dir: Option<PathBuf>,
152}
153
154/// The ONNX Runtime installer.
155pub struct OnnxRuntimeInstaller {
156    config: OnnxRuntimeConfig,
157}
158
159impl OnnxRuntimeInstaller {
160    /// Create a new ONNX Runtime installer with the given config.
161    pub fn new(config: OnnxRuntimeConfig) -> Self {
162        Self { config }
163    }
164
165    /// Create with default config.
166    pub fn with_defaults() -> Self {
167        Self::new(OnnxRuntimeConfig::default())
168    }
169
170    // -----------------------------------------------------------------------
171    // Dependencies (VAL-INSTALL-045)
172    // -----------------------------------------------------------------------
173
174    /// Get the list of required dependencies.
175    ///
176    /// ONNX Runtime depends on ROCm.
177    pub fn dependencies(&self) -> &[&str] {
178        &["rocm"]
179    }
180
181    // -----------------------------------------------------------------------
182    // ROCm version formatting
183    // -----------------------------------------------------------------------
184
185    /// Format ROCm version for ONNX Runtime (e.g., "7.2.0" -> "70200").
186    ///
187    /// The original script formats as Mmmpp: `printf "%d%02d%02d" major minor patch`.
188    pub fn format_rocm_version_for_ort(&self, rocm_version: &str) -> String {
189        let parts: Vec<&str> = rocm_version.split('.').collect();
190        let major: u32 = parts.first().and_then(|s| s.parse().ok()).unwrap_or(7);
191        let minor: u32 = parts.get(1).and_then(|s| s.parse().ok()).unwrap_or(2);
192        let patch: u32 = parts.get(2).and_then(|s| s.parse().ok()).unwrap_or(0);
193        format!("{:02}{:02}{:02}", major, minor, patch)
194    }
195
196    // -----------------------------------------------------------------------
197    // Prebuilt wheel install command
198    // -----------------------------------------------------------------------
199
200    /// Construct the pip install command for prebuilt onnxruntime-rocm wheel.
201    ///
202    /// The original script tries this first:
203    /// `pip install --upgrade --prefer-binary onnxruntime-rocm`
204    pub fn build_prebuilt_install_command(&self) -> ShellCommand {
205        ShellCommand {
206            program: self.config.python_bin.clone(),
207            args: vec![
208                "-m".to_string(),
209                "pip".to_string(),
210                "install".to_string(),
211                "--upgrade".to_string(),
212                "--prefer-binary".to_string(),
213                "onnxruntime-rocm".to_string(),
214            ],
215            env: vec![],
216            working_dir: None,
217        }
218    }
219
220    // -----------------------------------------------------------------------
221    // MIGraphX wheel install from AMD repo
222    // -----------------------------------------------------------------------
223
224    /// Construct the AMD manylinux repo URL for onnxruntime-migraphx.
225    ///
226    /// Pattern: `https://repo.radeon.com/rocm/manylinux/rocm-rel-{release}/`
227    ///
228    /// The URL points to a specific wheel matching the Python version and ROCm release.
229    /// ROCm 7.2.4 ships onnxruntime_migraphx 1.23.2.
230    pub fn build_migraphx_wheel_url(&self) -> String {
231        let release = self.config.rocm_release();
232        let python_bin = &self.config.python_bin;
233        // Extract python version from binary (e.g., "python3.12" -> "312")
234        let py_ver = if python_bin.contains('.') {
235            let parts: Vec<&str> = python_bin.split('.').collect();
236            if parts.len() >= 2 {
237                format!("{}{}", parts[0], parts[1])
238            } else {
239                "312".to_string()
240            }
241        } else {
242            "312".to_string()
243        };
244
245        format!(
246            "https://repo.radeon.com/rocm/manylinux/rocm-rel-{release}/\
247             onnxruntime_migraphx-1.23.2-cp{py_ver}-cp{py_ver}-\
248             manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl"
249        )
250    }
251
252    /// Construct the pip install command for onnxruntime-migraphx from AMD repo.
253    ///
254    /// This is the default install method — the AMD-provided wheel includes the
255    /// MIGraphX execution provider and is built against the matching ROCm version,
256    /// avoiding ABI incompatibility issues with PyPI's onnxruntime-rocm.
257    pub fn build_migraphx_install_command(&self) -> ShellCommand {
258        let url = self.build_migraphx_wheel_url();
259        ShellCommand {
260            program: self.config.python_bin.clone(),
261            args: vec![
262                "-m".to_string(),
263                "pip".to_string(),
264                "install".to_string(),
265                "--upgrade".to_string(),
266                url,
267            ],
268            env: vec![],
269            working_dir: None,
270        }
271    }
272
273    // -----------------------------------------------------------------------
274    // Model optimizer (ORT graph optimization for quantized models)
275    // -----------------------------------------------------------------------
276
277    /// Construct the Python command to run ORT graph optimization on an ONNX model.
278    ///
279    /// Applies `ORT_ENABLE_ALL` optimization level which fuses quantized ops
280    /// (DynamicQuantizeLinear, MatMulInteger, etc.) into custom ops that bypass
281    /// MIGraphX's broken kernels. The optimized model is saved alongside the original.
282    pub fn build_model_optimizer_command(&self, model_path: &str) -> ShellCommand {
283        let optimized_path = if model_path.ends_with(".onnx") {
284            format!("{}.optimized", model_path)
285        } else {
286            format!("{}.optimized", model_path)
287        };
288
289        let script = format!(
290            "import onnxruntime as ort; \
291             opts = ort.SessionOptions(); \
292             opts.graph_optimization_level = ort.GraphOptimizationLevel.ORT_ENABLE_ALL; \
293             opts.optimized_model_filepath = '{optimized_path}'; \
294             ort.InferenceSession('{model_path}', opts, providers=['CPUExecutionProvider']); \
295             print('Optimized: {model_path} -> {optimized_path}')"
296        );
297
298        ShellCommand {
299            program: self.config.python_bin.clone(),
300            args: vec!["-c".to_string(), script],
301            env: vec![],
302            working_dir: None,
303        }
304    }
305
306    // -----------------------------------------------------------------------
307    // Git clone command
308    // -----------------------------------------------------------------------
309
310    /// Construct the git clone command for ONNX Runtime.
311    ///
312    /// The original script clones:
313    /// `git clone --recursive https://github.com/microsoft/onnxruntime.git`
314    pub fn build_git_clone_command(&self) -> ShellCommand {
315        ShellCommand {
316            program: "git".to_string(),
317            args: vec![
318                "clone".to_string(),
319                "--recursive".to_string(),
320                "https://github.com/microsoft/onnxruntime.git".to_string(),
321            ],
322            env: vec![],
323            working_dir: Some(self.config.workdir()),
324        }
325    }
326
327    /// Construct the git checkout command for the stable tag.
328    ///
329    /// The original script checks out tag `v1.20.1`.
330    pub fn build_git_checkout_command(&self) -> ShellCommand {
331        ShellCommand {
332            program: "git".to_string(),
333            args: vec!["checkout".to_string(), "v1.20.1".to_string()],
334            env: vec![],
335            working_dir: Some(self.config.workdir().join("onnxruntime")),
336        }
337    }
338
339    /// Construct the git submodule update command.
340    pub fn build_git_submodule_command(&self) -> ShellCommand {
341        ShellCommand {
342            program: "git".to_string(),
343            args: vec![
344                "submodule".to_string(),
345                "update".to_string(),
346                "--init".to_string(),
347                "--recursive".to_string(),
348            ],
349            env: vec![],
350            working_dir: Some(self.config.workdir().join("onnxruntime")),
351        }
352    }
353
354    // -----------------------------------------------------------------------
355    // Build command (VAL-INSTALL-010)
356    // -----------------------------------------------------------------------
357
358    /// Construct the ONNX Runtime build.sh command with ROCm flags.
359    ///
360    /// The original script runs `./build.sh` with many flags:
361    /// - `--config Release`
362    /// - `--build_wheel`
363    /// - `--parallel $(nproc)-1`
364    /// - `--use_rocm --rocm_home /opt/rocm`
365    /// - `--rocm_version 70200`
366    /// - `--use_migraphx --migraphx_home /opt/rocm`
367    /// - `--cmake_extra_defines CMAKE_HIP_ARCHITECTURES=...`
368    /// - Various CMAKE isolation defines
369    /// - `--allow_running_as_root`
370    pub fn build_build_command(&self, rocm_env: &RocmEnv) -> ShellCommand {
371        let rocm_path = rocm_env
372            .path()
373            .map(|p| p.to_string_lossy().to_string())
374            .unwrap_or_else(|| "/opt/rocm".to_string());
375
376        let rocm_version = self.config.rocm_version();
377        let ort_rocm_version = self.format_rocm_version_for_ort(rocm_version);
378        let hip_archs = HipArchs::from_gpu_arch(self.config.gpu_arch());
379        let nproc = std::thread::available_parallelism()
380            .map(|n| n.get().saturating_sub(1))
381            .unwrap_or(3);
382
383        let mut args = vec![
384            "--config".to_string(),
385            "Release".to_string(),
386            "--build_wheel".to_string(),
387            "--parallel".to_string(),
388            nproc.to_string(),
389            "--use_rocm".to_string(),
390            "--rocm_home".to_string(),
391            rocm_path.clone(),
392            "--rocm_version".to_string(),
393            ort_rocm_version,
394            "--use_migraphx".to_string(),
395            "--migraphx_home".to_string(),
396            rocm_path.clone(),
397        ];
398
399        if self.config.use_preinstalled_eigen {
400            if let Some(ref eigen_path) = self.config.eigen_path {
401                args.push("--use_preinstalled_eigen".to_string());
402                args.push("--eigen_path".to_string());
403                args.push(eigen_path.to_string_lossy().to_string());
404            }
405        }
406
407        // CMAKE extra defines
408        let cmake_extra_defines = vec![
409            format!(
410                "CMAKE_HIP_ARCHITECTURES={}",
411                hip_archs.cmake_hip_architectures()
412            ),
413            "CMAKE_CXX_STANDARD=20".to_string(),
414            "onnxruntime_USE_EXTERNAL_ABSEIL=OFF".to_string(),
415            "CMAKE_DISABLE_FIND_PACKAGE_re2=ON".to_string(),
416            "CMAKE_FIND_PACKAGE_NO_PACKAGE_REGISTRY=ON".to_string(),
417            "CMAKE_FIND_PACKAGE_NO_SYSTEM_PACKAGE_REGISTRY=ON".to_string(),
418            "CMAKE_FIND_USE_PACKAGE_REGISTRY=OFF".to_string(),
419            "CMAKE_FIND_USE_SYSTEM_PACKAGE_REGISTRY=OFF".to_string(),
420            format!("CMAKE_PREFIX_PATH={}", rocm_path),
421            "re2_DIR=RE2_DIR-NOTFOUND".to_string(),
422            "CMAKE_POLICY_VERSION_MINIMUM=3.5".to_string(),
423        ];
424
425        for define in cmake_extra_defines {
426            args.push("--cmake_extra_defines".to_string());
427            args.push(define);
428        }
429
430        if self.config.use_preinstalled_eigen {
431            if let Some(ref eigen_path) = self.config.eigen_path {
432                args.push("--cmake_extra_defines".to_string());
433                args.push(format!(
434                    "FETCHCONTENT_SOURCE_DIR_EIGEN={}",
435                    eigen_path.to_string_lossy()
436                ));
437                args.push("--cmake_extra_defines".to_string());
438                args.push("FETCHCONTENT_TRY_FIND_PACKAGE_MODE=NEVER".to_string());
439            }
440        }
441
442        args.push("--allow_running_as_root".to_string());
443
444        let env = vec![
445            ("ROCM_HOME".to_string(), rocm_path.clone()),
446            ("ROCM_PATH".to_string(), rocm_path.clone()),
447            ("HIP_PATH".to_string(), rocm_path),
448            ("PYTHONPATH".to_string(), String::new()),
449            ("CMAKE_PREFIX_PATH".to_string(), "/opt/rocm".to_string()),
450            ("CMAKE_CXX_STANDARD".to_string(), "20".to_string()),
451            (
452                "CMAKE_FIND_PACKAGE_NO_PACKAGE_REGISTRY".to_string(),
453                "ON".to_string(),
454            ),
455            (
456                "CMAKE_FIND_PACKAGE_NO_SYSTEM_PACKAGE_REGISTRY".to_string(),
457                "ON".to_string(),
458            ),
459            (
460                "CMAKE_FIND_USE_PACKAGE_REGISTRY".to_string(),
461                "OFF".to_string(),
462            ),
463            (
464                "CMAKE_FIND_USE_SYSTEM_PACKAGE_REGISTRY".to_string(),
465                "OFF".to_string(),
466            ),
467        ];
468
469        ShellCommand {
470            program: "./build.sh".to_string(),
471            args,
472            env,
473            working_dir: Some(self.config.workdir().join("onnxruntime")),
474        }
475    }
476
477    // -----------------------------------------------------------------------
478    // Pip install from build output (VAL-INSTALL-011)
479    // -----------------------------------------------------------------------
480
481    /// Construct the pip uninstall command to remove all onnxruntime variants.
482    pub fn build_uninstall_command(&self) -> ShellCommand {
483        ShellCommand {
484            program: self.config.python_bin.clone(),
485            args: vec![
486                "-m".to_string(),
487                "pip".to_string(),
488                "uninstall".to_string(),
489                "-y".to_string(),
490                "onnxruntime".to_string(),
491                "onnxruntime-rocm".to_string(),
492                "onnxruntime-gpu".to_string(),
493                "onnxruntime-migraphx".to_string(),
494            ],
495            env: vec![],
496            working_dir: None,
497        }
498    }
499
500    /// Construct the pip install command for the built wheel.
501    ///
502    /// The original script installs from:
503    /// `build/Linux/Release/dist/*.whl`
504    pub fn build_wheel_install_command(&self) -> ShellCommand {
505        let wheel_path = self
506            .config
507            .workdir()
508            .join("onnxruntime")
509            .join("build/Linux/Release/dist/*.whl");
510
511        ShellCommand {
512            program: self.config.python_bin.clone(),
513            args: vec![
514                "-m".to_string(),
515                "pip".to_string(),
516                "install".to_string(),
517                wheel_path.to_string_lossy().to_string(),
518            ],
519            env: vec![],
520            working_dir: None,
521        }
522    }
523
524    // -----------------------------------------------------------------------
525    // Build error detection (VAL-INSTALL-014)
526    // -----------------------------------------------------------------------
527
528    /// Check build output for errors.
529    ///
530    /// Detects common build failure patterns and propagates as an error.
531    pub fn check_build_output(
532        &self,
533        stdout: &str,
534        stderr: &str,
535        exit_code: i32,
536    ) -> Result<(), String> {
537        if exit_code != 0 {
538            let error_patterns = [
539                "CMake Error",
540                "error:",
541                "FAILED:",
542                "fatal error:",
543                "Build failed",
544                "ninja: build stopped",
545                "Could NOT find",
546            ];
547
548            let combined = format!("{}\n{}", stdout, stderr);
549            let detected_errors: Vec<&str> = error_patterns
550                .iter()
551                .filter(|p| combined.contains(*p))
552                .copied()
553                .collect();
554
555            if detected_errors.is_empty() {
556                return Err(format!(
557                    "ONNX Runtime build failed with exit code {} (no specific error pattern detected)",
558                    exit_code
559                ));
560            }
561
562            return Err(format!(
563                "ONNX Runtime build failed with exit code {}. Detected errors: {}",
564                exit_code,
565                detected_errors.join(", ")
566            ));
567        }
568
569        Ok(())
570    }
571}
572
573// ===========================================================================
574// Tests
575// ===========================================================================
576
577#[cfg(test)]
578mod tests {
579    use super::*;
580    use std::path::PathBuf;
581
582    // --- VAL-INSTALL-010: ONNX Runtime correct cmake command ---
583
584    #[test]
585    fn test_build_command_has_rocm_flags() {
586        let installer = OnnxRuntimeInstaller::new(OnnxRuntimeConfig {
587            rocm_version: Some("7.2.0".to_string()),
588            gpu_arch: Some("gfx1100".to_string()),
589            ..Default::default()
590        });
591        let rocm_env = RocmEnv::from_known(Some(PathBuf::from("/opt/rocm")), "7.2.0".to_string());
592        let cmd = installer.build_build_command(&rocm_env);
593
594        assert_eq!(cmd.program, "./build.sh");
595        assert!(cmd.args.contains(&"--use_rocm".to_string()));
596        assert!(cmd.args.contains(&"--rocm_home".to_string()));
597        assert!(cmd.args.iter().any(|a| a == "/opt/rocm"));
598        assert!(cmd.args.contains(&"--use_migraphx".to_string()));
599        assert!(cmd.args.contains(&"--build_wheel".to_string()));
600        assert!(cmd.args.contains(&"--allow_running_as_root".to_string()));
601    }
602
603    #[test]
604    fn test_build_command_has_rocm_version() {
605        let installer = OnnxRuntimeInstaller::new(OnnxRuntimeConfig {
606            rocm_version: Some("7.2.0".to_string()),
607            ..Default::default()
608        });
609        let rocm_env = RocmEnv::from_known(Some(PathBuf::from("/opt/rocm")), "7.2.0".to_string());
610        let cmd = installer.build_build_command(&rocm_env);
611
612        assert!(cmd.args.contains(&"--rocm_version".to_string()));
613        // 7.2.0 -> "070200"
614        assert!(cmd.args.contains(&"070200".to_string()));
615    }
616
617    #[test]
618    fn test_build_command_has_hip_architectures() {
619        let installer = OnnxRuntimeInstaller::new(OnnxRuntimeConfig {
620            gpu_arch: Some("gfx1100".to_string()),
621            ..Default::default()
622        });
623        let rocm_env = RocmEnv::from_known(Some(PathBuf::from("/opt/rocm")), "7.2.0".to_string());
624        let cmd = installer.build_build_command(&rocm_env);
625
626        assert!(cmd
627            .args
628            .iter()
629            .any(|a| a.contains("CMAKE_HIP_ARCHITECTURES=gfx1100;gfx1101;gfx1102;gfx1103")));
630    }
631
632    #[test]
633    fn test_build_command_has_cmake_extra_defines() {
634        let installer = OnnxRuntimeInstaller::with_defaults();
635        let rocm_env = RocmEnv::from_known(Some(PathBuf::from("/opt/rocm")), "7.2.0".to_string());
636        let cmd = installer.build_build_command(&rocm_env);
637
638        assert!(cmd
639            .args
640            .iter()
641            .any(|a| a.contains("onnxruntime_USE_EXTERNAL_ABSEIL=OFF")));
642        assert!(cmd
643            .args
644            .iter()
645            .any(|a| a.contains("CMAKE_DISABLE_FIND_PACKAGE_re2=ON")));
646        assert!(cmd
647            .args
648            .iter()
649            .any(|a| a.contains("CMAKE_POLICY_VERSION_MINIMUM=3.5")));
650    }
651
652    // --- C++20 standard fix (fix-onnx-cmake-cxx20) ---
653
654    #[test]
655    fn test_build_command_includes_cmake_cxx_standard_20() {
656        let installer = OnnxRuntimeInstaller::with_defaults();
657        let rocm_env = RocmEnv::from_known(Some(PathBuf::from("/opt/rocm")), "7.2.0".to_string());
658        let cmd = installer.build_build_command(&rocm_env);
659
660        // The cmake command must include CMAKE_CXX_STANDARD=20 to fix
661        // C++20 standard library test failures during cmake configuration.
662        assert!(
663            cmd.args.iter().any(|a| a.contains("CMAKE_CXX_STANDARD=20")),
664            "build command must include CMAKE_CXX_STANDARD=20 cmake extra define, got args: {:?}",
665            cmd.args
666        );
667    }
668
669    #[test]
670    fn test_build_command_cmake_cxx_standard_is_cmake_extra_define() {
671        let installer = OnnxRuntimeInstaller::with_defaults();
672        let rocm_env = RocmEnv::from_known(Some(PathBuf::from("/opt/rocm")), "7.2.0".to_string());
673        let cmd = installer.build_build_command(&rocm_env);
674
675        // Find the CMAKE_CXX_STANDARD=20 arg and verify it's preceded by --cmake_extra_defines
676        let define_idx = cmd
677            .args
678            .iter()
679            .position(|a| a.contains("CMAKE_CXX_STANDARD=20"));
680        assert!(
681            define_idx.is_some(),
682            "CMAKE_CXX_STANDARD=20 not found in args"
683        );
684
685        let idx = define_idx.unwrap();
686        assert!(
687            idx > 0 && cmd.args[idx - 1] == "--cmake_extra_defines",
688            "CMAKE_CXX_STANDARD=20 must be preceded by --cmake_extra_defines"
689        );
690    }
691
692    #[test]
693    fn test_build_command_cmake_cxx_standard_env_var() {
694        let installer = OnnxRuntimeInstaller::with_defaults();
695        let rocm_env = RocmEnv::from_known(Some(PathBuf::from("/opt/rocm")), "7.2.0".to_string());
696        let cmd = installer.build_build_command(&rocm_env);
697
698        // The CXX standard should also be set as an environment variable for
699        // cmake to pick up during the C++20 standard library test.
700        assert!(
701            cmd.env
702                .iter()
703                .any(|(k, v)| k == "CMAKE_CXX_STANDARD" && v == "20"),
704            "build command env must include CMAKE_CXX_STANDARD=20, got env: {:?}",
705            cmd.env
706        );
707    }
708
709    #[test]
710    fn test_build_command_has_parallel_flag() {
711        let installer = OnnxRuntimeInstaller::with_defaults();
712        let rocm_env = RocmEnv::from_known(Some(PathBuf::from("/opt/rocm")), "7.2.0".to_string());
713        let cmd = installer.build_build_command(&rocm_env);
714
715        assert!(cmd.args.contains(&"--parallel".to_string()));
716    }
717
718    #[test]
719    fn test_build_command_env_vars() {
720        let installer = OnnxRuntimeInstaller::with_defaults();
721        let rocm_env = RocmEnv::from_known(Some(PathBuf::from("/opt/rocm")), "7.2.0".to_string());
722        let cmd = installer.build_build_command(&rocm_env);
723
724        assert!(cmd
725            .env
726            .iter()
727            .any(|(k, v)| k == "ROCM_HOME" && v == "/opt/rocm"));
728        assert!(cmd
729            .env
730            .iter()
731            .any(|(k, v)| k == "ROCM_PATH" && v == "/opt/rocm"));
732        assert!(cmd
733            .env
734            .iter()
735            .any(|(k, v)| k == "HIP_PATH" && v == "/opt/rocm"));
736        assert!(cmd
737            .env
738            .iter()
739            .any(|(k, v)| k == "PYTHONPATH" && v.is_empty()));
740    }
741
742    #[test]
743    fn test_build_command_with_eigen() {
744        let installer = OnnxRuntimeInstaller::new(OnnxRuntimeConfig {
745            use_preinstalled_eigen: true,
746            eigen_path: Some(PathBuf::from("/usr/include/eigen3")),
747            ..Default::default()
748        });
749        let rocm_env = RocmEnv::from_known(Some(PathBuf::from("/opt/rocm")), "7.2.0".to_string());
750        let cmd = installer.build_build_command(&rocm_env);
751
752        assert!(cmd.args.contains(&"--use_preinstalled_eigen".to_string()));
753        assert!(cmd.args.iter().any(|a| a.contains("/usr/include/eigen3")));
754        assert!(cmd
755            .args
756            .iter()
757            .any(|a| a.contains("FETCHCONTENT_SOURCE_DIR_EIGEN")));
758    }
759
760    // --- VAL-INSTALL-011: ONNX Runtime pip install from build output ---
761
762    #[test]
763    fn test_wheel_install_command() {
764        let installer = OnnxRuntimeInstaller::with_defaults();
765        let cmd = installer.build_wheel_install_command();
766
767        assert_eq!(cmd.program, "python3");
768        assert!(cmd.args.contains(&"-m".to_string()));
769        assert!(cmd.args.contains(&"pip".to_string()));
770        assert!(cmd.args.contains(&"install".to_string()));
771        assert!(cmd
772            .args
773            .iter()
774            .any(|a| a.contains("build/Linux/Release/dist/*.whl")));
775    }
776
777    #[test]
778    fn test_uninstall_command() {
779        let installer = OnnxRuntimeInstaller::with_defaults();
780        let cmd = installer.build_uninstall_command();
781
782        assert!(cmd.args.contains(&"uninstall".to_string()));
783        assert!(cmd.args.contains(&"-y".to_string()));
784        assert!(cmd.args.contains(&"onnxruntime".to_string()));
785        assert!(cmd.args.contains(&"onnxruntime-rocm".to_string()));
786        assert!(cmd.args.contains(&"onnxruntime-gpu".to_string()));
787        assert!(cmd.args.contains(&"onnxruntime-migraphx".to_string()));
788    }
789
790    // --- Prebuilt wheel install ---
791
792    #[test]
793    fn test_prebuilt_install_command() {
794        let installer = OnnxRuntimeInstaller::with_defaults();
795        let cmd = installer.build_prebuilt_install_command();
796
797        assert_eq!(cmd.program, "python3");
798        assert!(cmd.args.contains(&"-m".to_string()));
799        assert!(cmd.args.contains(&"pip".to_string()));
800        assert!(cmd.args.contains(&"--upgrade".to_string()));
801        assert!(cmd.args.contains(&"--prefer-binary".to_string()));
802        assert!(cmd.args.contains(&"onnxruntime-rocm".to_string()));
803    }
804
805    // --- Git commands ---
806
807    #[test]
808    fn test_git_clone_command() {
809        let installer = OnnxRuntimeInstaller::with_defaults();
810        let cmd = installer.build_git_clone_command();
811        assert_eq!(cmd.program, "git");
812        assert!(cmd.args.contains(&"clone".to_string()));
813        assert!(cmd.args.contains(&"--recursive".to_string()));
814        assert!(cmd.args.iter().any(|a| a.contains("microsoft/onnxruntime")));
815    }
816
817    #[test]
818    fn test_git_checkout_command() {
819        let installer = OnnxRuntimeInstaller::with_defaults();
820        let cmd = installer.build_git_checkout_command();
821        assert!(cmd.args.contains(&"v1.20.1".to_string()));
822    }
823
824    // --- ROCm version formatting ---
825
826    #[test]
827    fn test_rocm_version_formatting() {
828        let installer = OnnxRuntimeInstaller::with_defaults();
829        assert_eq!(installer.format_rocm_version_for_ort("7.2.0"), "070200");
830        assert_eq!(installer.format_rocm_version_for_ort("7.0.0"), "070000");
831        assert_eq!(installer.format_rocm_version_for_ort("7.2.1"), "070201");
832    }
833
834    // --- HipArchs ---
835
836    #[test]
837    fn test_hip_archs_gfx1030() {
838        let archs = HipArchs::from_gpu_arch("gfx1030");
839        assert_eq!(archs.cmake_hip_architectures(), "gfx1030");
840    }
841
842    #[test]
843    fn test_hip_archs_gfx1100() {
844        let archs = HipArchs::from_gpu_arch("gfx1100");
845        assert_eq!(
846            archs.cmake_hip_architectures(),
847            "gfx1100;gfx1101;gfx1102;gfx1103"
848        );
849    }
850
851    #[test]
852    fn test_hip_archs_gfx1200() {
853        let archs = HipArchs::from_gpu_arch("gfx1200");
854        assert_eq!(archs.cmake_hip_architectures(), "gfx1200;gfx1201");
855    }
856
857    #[test]
858    fn test_hip_archs_default() {
859        let archs = HipArchs::from_gpu_arch("gfx900");
860        assert_eq!(archs.cmake_hip_architectures(), "gfx1100");
861    }
862
863    // --- VAL-INSTALL-045: ONNX Runtime declares dependency on ROCm ---
864
865    #[test]
866    fn test_dependencies() {
867        let installer = OnnxRuntimeInstaller::with_defaults();
868        let deps = installer.dependencies();
869        assert!(deps.contains(&"rocm"));
870    }
871
872    // --- VAL-INSTALL-014: Build error detection ---
873
874    #[test]
875    fn test_build_error_cmake() {
876        let installer = OnnxRuntimeInstaller::with_defaults();
877        let result = installer.check_build_output("", "CMake Error at cmake/CMakeLists.txt:42", 1);
878        assert!(result.is_err());
879        assert!(result.unwrap_err().contains("CMake Error"));
880    }
881
882    #[test]
883    fn test_build_error_ninja() {
884        let installer = OnnxRuntimeInstaller::with_defaults();
885        let result =
886            installer.check_build_output("ninja: build stopped: subcommand failed.", "", 1);
887        assert!(result.is_err());
888        assert!(result.unwrap_err().contains("ninja: build stopped"));
889    }
890
891    #[test]
892    fn test_build_error_success() {
893        let installer = OnnxRuntimeInstaller::with_defaults();
894        let result = installer.check_build_output("Build succeeded", "", 0);
895        assert!(result.is_ok());
896    }
897
898    // --- Config defaults ---
899
900    #[test]
901    fn test_config_defaults() {
902        let config = OnnxRuntimeConfig::default();
903        assert_eq!(config.python_bin, "python3");
904        assert!(!config.dry_run);
905        assert!(config.rocm_version.is_none());
906        assert!(config.rocm_release.is_none());
907        assert!(config.gpu_arch.is_none());
908        assert!(config.workdir.is_none());
909        assert!(!config.use_preinstalled_eigen);
910        assert!(config.eigen_path.is_none());
911        assert_eq!(config.install_method, OnnxInstallMethod::MigraphxWheel);
912    }
913
914    // --- MIGraphX wheel install ---
915
916    #[test]
917    fn test_migraphx_wheel_url_default() {
918        let installer = OnnxRuntimeInstaller::with_defaults();
919        let url = installer.build_migraphx_wheel_url();
920        assert!(url.contains("repo.radeon.com/rocm/manylinux/rocm-rel-7.2.4/"));
921        assert!(url.contains("onnxruntime_migraphx-1.23.2"));
922        assert!(url.contains("manylinux_2_27_x86_64"));
923    }
924
925    #[test]
926    fn test_migraphx_wheel_url_custom_release() {
927        let installer = OnnxRuntimeInstaller::new(OnnxRuntimeConfig {
928            rocm_release: Some("7.2.1".to_string()),
929            ..Default::default()
930        });
931        let url = installer.build_migraphx_wheel_url();
932        assert!(url.contains("rocm-rel-7.2.1/"));
933    }
934
935    #[test]
936    fn test_migraphx_install_command() {
937        let installer = OnnxRuntimeInstaller::with_defaults();
938        let cmd = installer.build_migraphx_install_command();
939        assert_eq!(cmd.program, "python3");
940        assert!(cmd.args.contains(&"-m".to_string()));
941        assert!(cmd.args.contains(&"pip".to_string()));
942        assert!(cmd.args.contains(&"install".to_string()));
943        assert!(cmd.args.iter().any(|a| a.contains("repo.radeon.com")));
944        assert!(cmd.args.iter().any(|a| a.contains("onnxruntime_migraphx")));
945    }
946
947    // --- Model optimizer ---
948
949    #[test]
950    fn test_model_optimizer_command() {
951        let installer = OnnxRuntimeInstaller::with_defaults();
952        let cmd = installer.build_model_optimizer_command("/path/to/model.onnx");
953        assert_eq!(cmd.program, "python3");
954        assert!(cmd.args.contains(&"-c".to_string()));
955        let script = &cmd.args[1];
956        assert!(script.contains("ORT_ENABLE_ALL"));
957        assert!(script.contains("/path/to/model.onnx"));
958        assert!(script.contains("/path/to/model.onnx.optimized"));
959    }
960
961    // --- OnnxInstallMethod ---
962
963    #[test]
964    fn test_install_method_default() {
965        assert_eq!(
966            OnnxInstallMethod::default(),
967            OnnxInstallMethod::MigraphxWheel
968        );
969    }
970}