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