1use crate::installers::common::{
35 log_warn, submit_build_report, BuildReport, BuildReportStatus, SealedToken,
36};
37use serde::Deserialize;
38use sha2::{Digest, Sha256};
39use std::fs;
40use std::io::{Read, Write};
41use std::path::{Path, PathBuf};
42use std::time::Instant;
43use tempfile::NamedTempFile;
44
45const DEFAULT_REPO_URL: &str = "https://github.com/scooter-lacroix/llama.cpp-turboquant-hip";
51
52const DEFAULT_BRANCH: &str = "main";
54
55const DEFAULT_BUILD_DIR: &str = "/tmp/llama-cpp-rocm-build";
57
58const DEFAULT_INSTALL_PREFIX: &str = ".mlstack/components/llama-cpp";
60
61const BUILD_TARGETS: &[&str] = &["llama-cli", "llama-bench", "llama-server"];
63
64pub const DETECTION_BINARY: &str = "llama-cli";
66
67pub const DETECTION_SUBCOMMAND: &str = "--help";
69
70#[derive(Debug, Clone, PartialEq, Eq)]
76pub enum HipArchs {
77 Gfx1030,
79 Gfx1100,
81 Gfx1200,
83 Default,
85}
86
87impl HipArchs {
88 pub fn from_gpu_arch(gpu_arch: &str) -> Self {
90 if gpu_arch.starts_with("gfx103") {
91 HipArchs::Gfx1030
92 } else if gpu_arch.starts_with("gfx110") {
93 HipArchs::Gfx1100
94 } else if gpu_arch.starts_with("gfx120") {
95 HipArchs::Gfx1200
96 } else {
97 HipArchs::Default
98 }
99 }
100
101 pub fn gpu_targets_for_channel(&self, channel: &str) -> String {
110 match (self, channel) {
111 (_, "legacy") => "gfx1030".to_string(),
113 (HipArchs::Gfx1030, "stable") => "gfx1030".to_string(),
115 (HipArchs::Gfx1100, "stable") => "gfx1030;gfx1100;gfx1101".to_string(),
116 (HipArchs::Gfx1200, "stable") => "gfx1030;gfx1100;gfx1101".to_string(),
117 (HipArchs::Default, "stable") => "gfx1030".to_string(),
118 (HipArchs::Gfx1030, _) => "gfx1030".to_string(),
120 (HipArchs::Gfx1100, _) => "gfx1030;gfx1100;gfx1101".to_string(),
121 (HipArchs::Gfx1200, _) => "gfx1030;gfx1100;gfx1101;gfx1200".to_string(),
122 (HipArchs::Default, _) => "gfx1030".to_string(),
123 }
124 }
125}
126
127#[derive(Debug, Clone, PartialEq, Eq)]
129pub enum RocmChannel {
130 Legacy,
131 Stable,
132 Latest,
133}
134
135impl RocmChannel {
136 pub fn parse_channel(channel: &str) -> Self {
138 match channel.to_lowercase().as_str() {
139 "legacy" => RocmChannel::Legacy,
140 "stable" => RocmChannel::Stable,
141 _ => RocmChannel::Latest,
142 }
143 }
144
145 pub fn enable_wmma_fa(&self) -> bool {
147 matches!(self, RocmChannel::Stable | RocmChannel::Latest)
148 }
149
150 pub fn label(&self) -> &'static str {
152 match self {
153 RocmChannel::Legacy => "legacy",
154 RocmChannel::Stable => "stable",
155 RocmChannel::Latest => "latest",
156 }
157 }
158}
159
160#[derive(Debug, Clone, PartialEq, Eq)]
162pub struct ShellCommand {
163 pub program: String,
165 pub args: Vec<String>,
167 pub env: Vec<(String, String)>,
169 pub working_dir: Option<PathBuf>,
171}
172
173#[derive(Debug, Clone)]
175pub struct LlamaCppConfig {
176 pub gpu_arch: String,
178 pub channel: String,
180 pub repo_url: Option<String>,
182 pub branch: String,
184 pub build_dir: Option<String>,
186 pub install_prefix: Option<String>,
188 pub dry_run: bool,
190}
191
192#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
193#[allow(dead_code)]
194pub struct ReleaseAsset {
195 pub url: String,
196 pub sha256: String,
197}
198
199#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
200#[serde(deny_unknown_fields)]
201pub struct ReleaseManifest {
202 pub version: String,
203 pub archs: ReleaseArchs,
204}
205
206#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
207#[serde(deny_unknown_fields)]
208pub struct ReleaseArchs {
209 pub gfx1030: ReleaseAsset,
210 pub gfx1100: ReleaseAsset,
211 pub gfx1200: ReleaseAsset,
212}
213
214#[derive(Debug, Clone, PartialEq, Eq)]
215pub struct PrebuiltInstallPlan {
216 pub manifest_version: String,
217 pub arch: String,
218 pub url: String,
219 pub sha256: String,
220 pub install_prefix: String,
221 pub binary_path: PathBuf,
222}
223
224#[derive(Debug, Clone, PartialEq, Eq)]
225pub struct SourceInstallPlan {
226 pub reason: String,
227 pub build_commands: Vec<ShellCommand>,
228 pub install_prefix: String,
229 pub binary_path: PathBuf,
230}
231
232#[derive(Debug, Clone, PartialEq, Eq)]
233pub enum InstallStrategy {
234 Prebuilt(PrebuiltInstallPlan),
235 Source(SourceInstallPlan),
236}
237
238impl Default for LlamaCppConfig {
239 fn default() -> Self {
240 Self {
241 gpu_arch: "gfx1100".to_string(),
242 channel: "latest".to_string(),
243 repo_url: None,
244 branch: DEFAULT_BRANCH.to_string(),
245 build_dir: None,
246 install_prefix: None,
247 dry_run: false,
248 }
249 }
250}
251
252impl LlamaCppConfig {
253 pub fn repo_url(&self) -> &str {
255 self.repo_url.as_deref().unwrap_or(DEFAULT_REPO_URL)
256 }
257
258 pub fn build_dir(&self) -> &str {
260 self.build_dir.as_deref().unwrap_or(DEFAULT_BUILD_DIR)
261 }
262
263 pub fn install_prefix(&self) -> &str {
265 self.install_prefix
266 .as_deref()
267 .unwrap_or(DEFAULT_INSTALL_PREFIX)
268 }
269
270 pub fn install_bin_dir(&self, home: &str) -> PathBuf {
272 PathBuf::from(home).join(self.install_prefix()).join("bin")
273 }
274
275 pub fn detection_binary_path(&self, home: &str) -> PathBuf {
277 self.install_bin_dir(home).join(DETECTION_BINARY)
278 }
279}
280
281#[derive(Debug, Clone)]
283pub struct LlamaCppInstaller {
284 config: LlamaCppConfig,
285}
286
287impl Default for LlamaCppInstaller {
288 fn default() -> Self {
289 Self::new(LlamaCppConfig::default())
290 }
291}
292
293impl LlamaCppInstaller {
294 pub fn new(config: LlamaCppConfig) -> Self {
296 Self { config }
297 }
298
299 pub fn with_defaults() -> Self {
301 Self::new(LlamaCppConfig::default())
302 }
303
304 pub fn dependencies(&self) -> &[&str] {
312 &["rocm"]
313 }
314
315 pub fn check_latest_release(&self) -> Option<ReleaseManifest> {
319 let mut token = SealedToken::from_env();
320 let result = self.fetch_latest_release_manifest(token.as_str());
321 token.purge();
322 result
323 }
324
325 pub fn release_asset_for_arch<'a>(
326 &self,
327 manifest: &'a ReleaseManifest,
328 gpu_arch: &str,
329 ) -> Option<&'a ReleaseAsset> {
330 match gpu_arch {
331 "gfx1030" => Some(&manifest.archs.gfx1030),
332 "gfx1100" | "gfx1101" => Some(&manifest.archs.gfx1100),
333 "gfx1200" | "gfx1201" => Some(&manifest.archs.gfx1200),
334 _ => None,
335 }
336 }
337
338 pub fn validate_repo_access(&self) -> Result<(), String> {
347 let mut github_token = Some(SealedToken::from_env());
348
349 let temp_dir = tempfile::tempdir().map_err(|e| e.to_string())?;
351 let temp_path = temp_dir
352 .path()
353 .to_str()
354 .ok_or("Failed to create temp dir")?
355 .to_string();
356
357 let mut cmd = if let Some(ref token) = github_token {
359 let credential_helper = r#"!sh -c "echo username=git; echo password=$GITHUB_TOKEN""#;
361 let mut git_cmd = std::process::Command::new("git");
362 git_cmd
363 .arg("clone")
364 .arg("--depth")
365 .arg("1")
366 .arg("--branch")
367 .arg(&self.config.branch)
368 .arg(self.config.repo_url())
369 .arg(&temp_path)
370 .arg("-c")
371 .arg(format!("credential.helper={}", credential_helper))
372 .env("GITHUB_TOKEN", token.as_str())
373 .env("GIT_TERMINAL_PROMPT", "0");
374 git_cmd
375 } else {
376 let mut git_cmd = std::process::Command::new("git");
378 git_cmd
379 .arg("clone")
380 .arg("--depth")
381 .arg("1")
382 .arg("--branch")
383 .arg(&self.config.branch)
384 .arg(self.config.repo_url())
385 .arg(&temp_path);
386 git_cmd
387 };
388
389 let output = match cmd.output() {
390 Ok(output) => output,
391 Err(e) => {
392 if let Some(token) = github_token.as_mut() {
393 token.purge();
394 }
395 return Err(e.to_string());
396 }
397 };
398
399 if !output.status.success() {
400 let stderr = String::from_utf8_lossy(&output.stderr);
401 if let Some(token) = github_token.as_mut() {
402 token.purge();
403 }
404 if stderr.contains("authentication") || stderr.contains("403") || stderr.contains("401")
405 {
406 return Err(
407 "authentication failed for private repo. If you have access, set the build-time GitHub token.".to_string(),
408 );
409 } else {
410 return Err("Failed to clone repository".to_string());
411 }
412 }
413
414 if let Some(token) = github_token.as_mut() {
415 token.purge();
416 }
417 Ok(())
418 }
419
420 fn fetch_latest_release_manifest(&self, token: &str) -> Option<ReleaseManifest> {
421 let url =
422 "https://api.github.com/repos/scooter-lacroix/llama.cpp-turboquant-hip/releases/latest";
423 let response = ureq::get(url)
424 .header("Authorization", format!("Bearer {}", token))
425 .header("Accept", "application/vnd.github+json")
426 .header("X-GitHub-Api-Version", "2022-11-28")
427 .header("User-Agent", "rusty-stack")
428 .call();
429
430 let mut response = match response {
431 Ok(response) => response,
432 Err(_) => return None,
433 };
434
435 if response.status() == 404 {
436 return None;
437 }
438
439 if !response.status().is_success() {
440 return None;
441 }
442
443 response.body_mut().read_json::<ReleaseManifest>().ok()
444 }
445
446 pub fn cmake_flags(&self) -> Vec<String> {
454 let hip_archs = HipArchs::from_gpu_arch(&self.config.gpu_arch);
455 let channel = RocmChannel::parse_channel(&self.config.channel);
456 let gpu_targets = hip_archs.gpu_targets_for_channel(channel.label());
457
458 let mut flags = vec![
459 "-DGGML_HIP=ON".to_string(),
460 format!("-DGPU_TARGETS={}", gpu_targets),
461 "-DGGML_CUDA=OFF".to_string(),
463 "-DGGML_VULKAN=OFF".to_string(),
464 "-DGGML_METAL=OFF".to_string(),
465 ];
466
467 if channel.enable_wmma_fa() {
469 flags.push("-DGGML_HIP_RDNA3_PROBES=ON".to_string());
470 flags.push("-DGGML_HIP_ROCWMMA_FATTN=ON".to_string());
471 }
472
473 flags
474 }
475
476 fn expected_gpu_targets(&self) -> String {
477 let hip_archs = HipArchs::from_gpu_arch(&self.config.gpu_arch);
478 let channel = RocmChannel::parse_channel(&self.config.channel);
479 hip_archs.gpu_targets_for_channel(channel.label())
480 }
481
482 pub fn build_commands(&self, home: &str) -> Vec<ShellCommand> {
488 self.build_commands_with_auth(home, None)
489 }
490
491 fn build_source_commands_from_env(&self, home: &str) -> Vec<ShellCommand> {
493 let mut token = SealedToken::from_env();
494 if token.as_str().is_empty() {
495 token.purge();
496 self.build_commands_with_auth(home, None)
497 } else {
498 self.build_commands_with_auth(home, Some(token))
499 }
500 }
501
502 pub fn resolve_install_strategy(
504 &self,
505 home: &str,
506 manifest: Option<&ReleaseManifest>,
507 ) -> InstallStrategy {
508 let install_prefix = format!("{}/{}", home, self.config.install_prefix());
509 let binary_path = self.config.detection_binary_path(home);
510 if let Some(manifest) = manifest {
511 if let Some(asset) = self.release_asset_for_arch(manifest, &self.config.gpu_arch) {
512 return InstallStrategy::Prebuilt(PrebuiltInstallPlan {
513 manifest_version: manifest.version.clone(),
514 arch: self.config.gpu_arch.clone(),
515 url: asset.url.clone(),
516 sha256: asset.sha256.clone(),
517 install_prefix,
518 binary_path,
519 });
520 }
521 }
522
523 let reason = if manifest.is_some() {
524 format!(
525 "No pre-built binary available for {}; falling back to source compile.",
526 self.config.gpu_arch
527 )
528 } else {
529 format!(
530 "Unable to fetch release manifest or match {}; falling back to source compile.",
531 self.config.gpu_arch
532 )
533 };
534
535 InstallStrategy::Source(SourceInstallPlan {
536 reason,
537 build_commands: self.build_source_commands_from_env(home),
538 install_prefix,
539 binary_path,
540 })
541 }
542
543 fn verify_sha256(path: &std::path::Path, expected: &str) -> Result<bool, String> {
544 let mut file = fs::File::open(path).map_err(|e| e.to_string())?;
545 let mut hasher = Sha256::new();
546 let mut buffer = [0u8; 8192];
547 loop {
548 let count = file.read(&mut buffer).map_err(|e| e.to_string())?;
549 if count == 0 {
550 break;
551 }
552 hasher.update(&buffer[..count]);
553 }
554 let actual = Self::sha256_hex_bytes(&hasher.finalize());
555 Ok(actual.eq_ignore_ascii_case(expected))
556 }
557
558 pub fn download_prebuilt_binary(&self, plan: &PrebuiltInstallPlan) -> Result<(), String> {
559 let mut temp = NamedTempFile::new().map_err(|e| e.to_string())?;
560 let response = match ureq::get(&plan.url).call() {
561 Ok(response) => response,
562 Err(err) => {
563 log_warn(&format!(
564 "Prebuilt llama.cpp download failed; falling back to source compile: {}",
565 err
566 ));
567 return Err("download failed".to_string());
568 }
569 };
570
571 if !response.status().is_success() {
572 log_warn("Prebuilt llama.cpp download failed; falling back to source compile.");
573 return Err("download failed".to_string());
574 }
575
576 let downloaded = response
577 .into_body()
578 .read_to_vec()
579 .map_err(|e| e.to_string())?;
580 temp.write_all(&downloaded).map_err(|e| e.to_string())?;
581 temp.flush().map_err(|e| e.to_string())?;
582
583 if !Self::verify_sha256(temp.path(), &plan.sha256)? {
584 let message = format!(
585 "Checksum mismatch for {} binary: expected {}, got {}. Download may be corrupted. Falling back to source build.",
586 plan.arch,
587 plan.sha256,
588 Self::sha256_hex(temp.path())?
589 );
590 log_warn(&message);
591 return Err(message);
592 }
593
594 fs::create_dir_all(&plan.install_prefix).map_err(|e| e.to_string())?;
595 let status = std::process::Command::new("tar")
596 .arg("-xzf")
597 .arg(temp.path())
598 .arg("-C")
599 .arg(&plan.install_prefix)
600 .status()
601 .map_err(|e| e.to_string())?;
602
603 if !status.success() {
604 return Err("failed to extract prebuilt archive".to_string());
605 }
606
607 verify_binary_linkage(&plan.binary_path)?;
610
611 temp.close().map_err(|e| e.to_string())?;
612 Ok(())
613 }
614
615 fn sha256_hex(path: &std::path::Path) -> Result<String, String> {
616 let mut file = fs::File::open(path).map_err(|e| e.to_string())?;
617 let mut hasher = Sha256::new();
618 let mut buffer = [0u8; 8192];
619 loop {
620 let count = file.read(&mut buffer).map_err(|e| e.to_string())?;
621 if count == 0 {
622 break;
623 }
624 hasher.update(&buffer[..count]);
625 }
626 Ok(Self::sha256_hex_bytes(&hasher.finalize()))
627 }
628
629 fn sha256_hex_bytes(bytes: &[u8]) -> String {
630 bytes.iter().map(|byte| format!("{:02x}", byte)).collect()
631 }
632
633 pub fn purge_source_artifacts(&self, home: &str, binary_placed: bool) -> Result<(), String> {
640 let build_dir = self.config.build_dir().to_string();
641 let install_bin = self.config.detection_binary_path(home);
642 let mut had_error = None::<String>;
643 let purge_plan = [
644 (
645 "source repository metadata",
646 std::process::Command::new("find")
647 .args([
648 format!("{}/.mlstack", home),
649 "-type".to_string(),
650 "d".to_string(),
651 "-name".to_string(),
652 ".git".to_string(),
653 "-prune".to_string(),
654 "-exec".to_string(),
655 "rm".to_string(),
656 "-rf".to_string(),
657 "{}".to_string(),
658 ";".to_string(),
659 ])
660 .output(),
661 ),
662 (
663 "git credential store",
664 std::process::Command::new("git")
665 .args([
666 "credential-store".to_string(),
667 "erase".to_string(),
668 "--file".to_string(),
669 format!("{}/.git-credentials", home),
670 ])
671 .output(),
672 ),
673 (
674 "git credential cache",
675 std::process::Command::new("git")
676 .args(["credential-cache".to_string(), "exit".to_string()])
677 .output(),
678 ),
679 (
680 "source build tree",
681 std::process::Command::new("rm")
682 .args(["-rf".to_string(), build_dir.clone()])
683 .output(),
684 ),
685 ];
686
687 for (label, output) in purge_plan {
688 match output {
689 Ok(output) if output.status.success() => {}
690 Ok(output) => {
691 let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
692 had_error = Some(if stderr.is_empty() {
693 format!("{} purge failed", label)
694 } else {
695 stderr
696 });
697 break;
698 }
699 Err(err) => {
700 had_error = Some(format!("{} purge failed: {}", label, err));
701 break;
702 }
703 }
704 }
705
706 if let Some(err) = had_error {
707 if binary_placed && install_bin.exists() {
708 log_warn(&format!(
709 "llama.cpp source purge completed with warnings: {}",
710 err
711 ));
712 return Ok(());
713 }
714 return Err(format!(
715 "Failed to purge llama.cpp source artifacts; please remove {} and retry: {}",
716 build_dir, err
717 ));
718 }
719
720 Ok(())
721 }
722
723 pub fn build_commands_with_auth(
725 &self,
726 home: &str,
727 token: Option<SealedToken>,
728 ) -> Vec<ShellCommand> {
729 let build_dir = self.config.build_dir().to_string();
730 let install_prefix = format!("{}/{}", home, self.config.install_prefix());
731
732 let mut commands = Vec::new();
734 commands.push(self.mkdir_build_dir(&build_dir));
735
736 if let Some(mut token) = token {
737 commands.push(self.git_clone_with_auth(&build_dir, token.as_str()));
738 token.purge();
739 } else {
740 commands.push(self.git_clone(&build_dir));
741 }
742
743 commands.push(self.git_checkout(&build_dir));
744 commands.push(self.cmake_configure(&build_dir, &install_prefix));
745 commands.push(self.cmake_build(&build_dir));
746 commands.push(self.cmake_install(&build_dir));
747 commands.push(self.purge_source_artifacts_command(home));
748
749 commands
750 }
751
752 pub fn install(&self, home: &str, manifest: Option<&ReleaseManifest>) -> Result<(), String> {
753 let start = Instant::now();
754 match self.resolve_install_strategy(home, manifest) {
755 InstallStrategy::Prebuilt(plan) => match self.download_prebuilt_binary(&plan) {
756 Ok(()) => {
757 self.submit_telemetry(home, start, true, Some(&plan.binary_path))
758 .ok();
759 Ok(())
760 }
761 Err(err) if err.contains("checksum mismatch") => {
762 log_warn(&format!("{err} Falling back to source compile."));
763 let result = self.run_source_install(home);
764 if result.is_ok() {
765 self.submit_telemetry(home, start, false, None).ok();
766 }
767 result
768 }
769 Err(_) => {
770 log_warn("Prebuilt download unavailable; using source compile.");
771 let result = self.run_source_install(home);
772 if result.is_ok() {
773 self.submit_telemetry(home, start, false, None).ok();
774 }
775 result
776 }
777 },
778 InstallStrategy::Source(plan) => {
779 let result = self.run_source_install(home);
780 if result.is_ok() {
781 self.submit_telemetry(home, start, false, Some(&plan.binary_path))
782 .ok();
783 }
784 result
785 }
786 }
787 }
788
789 pub fn run_source_install(&self, home: &str) -> Result<(), String> {
790 let cuda_artifacts = detect_cuda_toolkit_presence();
792 if !cuda_artifacts.is_empty() {
793 log_warn(&format!(
794 "NVIDIA CUDA toolkit detected on this system ({}). Rusty Llama builds exclusively with ROCm/HIP; CUDA will not be used.",
795 cuda_artifacts.join(", ")
796 ));
797 }
798
799 let commands = self.build_source_commands_from_env(home);
800 if commands.is_empty() {
801 return Err("no source install commands available".to_string());
802 }
803
804 for command in commands {
805 let is_cmake_configure = command.program == "cmake"
806 && command.args.iter().any(|arg| arg == "-B")
807 && command.args.iter().any(|arg| arg == "-S");
808 let is_cmake_install =
809 command.program == "cmake" && command.args.iter().any(|arg| arg == "--install");
810 let command_working_dir = command.working_dir.clone();
811
812 let mut process = std::process::Command::new(&command.program);
813 process.args(&command.args);
814 for (key, value) in &command.env {
815 process.env(key, value);
816 }
817 if let Some(dir) = &command.working_dir {
818 process.current_dir(dir);
819 }
820
821 let output = process.output().map_err(|err| {
822 format!(
823 "failed to run source install command '{}': {}",
824 command.program, err
825 )
826 })?;
827
828 if !output.status.success() {
829 let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
830 let stdout = String::from_utf8_lossy(&output.stdout).trim().to_string();
831 let mut details = String::new();
832 if !stderr.is_empty() {
833 details.push_str(&stderr);
834 }
835 if !stdout.is_empty() {
836 if !details.is_empty() {
837 details.push_str(" | ");
838 }
839 details.push_str(&stdout);
840 }
841
842 return Err(if details.is_empty() {
843 format!("source install command '{}' failed", command.program)
844 } else {
845 format!(
846 "source install command '{}' failed: {}",
847 command.program, details
848 )
849 });
850 }
851
852 if is_cmake_configure {
853 let cache_path = command_working_dir
854 .as_deref()
855 .unwrap_or_else(|| Path::new("."))
856 .join("build")
857 .join("CMakeCache.txt");
858 validate_cmake_cache(&cache_path, &self.expected_gpu_targets())?;
859 }
860
861 if is_cmake_install {
862 let bin_path = resolve_install_bin_dir(home).join(DETECTION_BINARY);
865 verify_binary_linkage(&bin_path)?;
866 }
867 }
868
869 Ok(())
870 }
871
872 fn purge_source_artifacts_command(&self, home: &str) -> ShellCommand {
873 let build_dir = self.config.build_dir().to_string();
874 let mlstack_dir = format!("{}/.mlstack", home);
875 let credentials_file = format!("{}/.git-credentials", home);
876 ShellCommand {
877 program: "sh".to_string(),
878 args: vec![
879 "-c".to_string(),
880 format!(
881 "find '{}' -type d -name .git -prune -exec rm -rf '{{}}' ';' ; git credential-store erase --file '{}' ; git credential-cache exit ; rm -rf '{}'",
882 mlstack_dir, credentials_file, build_dir
883 ),
884 ],
885 env: vec![],
886 working_dir: None,
887 }
888 }
889
890 fn git_clone_with_auth(&self, build_dir: &str, token: &str) -> ShellCommand {
896 let credential_helper = r#"!sh -c "echo username=git; echo password=$GITHUB_TOKEN""#;
898 ShellCommand {
899 program: "git".to_string(),
900 args: vec![
901 "clone".to_string(),
902 "--depth".to_string(),
903 "1".to_string(),
904 "--branch".to_string(),
905 self.config.branch.clone(),
906 self.config.repo_url().to_string(),
907 build_dir.to_string(),
908 "-c".to_string(),
909 format!("credential.helper={}", credential_helper),
910 ],
911 env: vec![
912 ("GITHUB_TOKEN".to_string(), token.to_string()),
913 ("GIT_TERMINAL_PROMPT".to_string(), "0".to_string()),
914 ],
915 working_dir: None,
916 }
917 }
918
919 fn mkdir_build_dir(&self, build_dir: &str) -> ShellCommand {
920 ShellCommand {
921 program: "mkdir".to_string(),
922 args: vec!["-p".to_string(), build_dir.to_string()],
923 env: vec![],
924 working_dir: None,
925 }
926 }
927
928 fn git_clone(&self, build_dir: &str) -> ShellCommand {
929 ShellCommand {
930 program: "git".to_string(),
931 args: vec![
932 "clone".to_string(),
933 "--depth".to_string(),
934 "1".to_string(),
935 "--branch".to_string(),
936 self.config.branch.clone(),
937 self.config.repo_url().to_string(),
938 build_dir.to_string(),
939 ],
940 env: vec![],
941 working_dir: None,
942 }
943 }
944
945 fn git_checkout(&self, build_dir: &str) -> ShellCommand {
946 ShellCommand {
947 program: "git".to_string(),
948 args: vec!["checkout".to_string(), self.config.branch.clone()],
949 env: vec![],
950 working_dir: Some(PathBuf::from(build_dir)),
951 }
952 }
953
954 fn cmake_configure(&self, build_dir: &str, install_prefix: &str) -> ShellCommand {
955 let mut args = vec![
956 "-B".to_string(),
957 "build".to_string(),
958 "-S".to_string(),
959 ".".to_string(),
960 ];
961 args.extend(self.cmake_flags());
962 args.push(format!("-DCMAKE_INSTALL_PREFIX={}", install_prefix));
963
964 ShellCommand {
965 program: "cmake".to_string(),
966 args,
967 env: vec![],
968 working_dir: Some(PathBuf::from(build_dir)),
969 }
970 }
971
972 fn cmake_build(&self, build_dir: &str) -> ShellCommand {
973 let mut args = vec!["--build".to_string(), "build".to_string(), "-j".to_string()];
974 for target in BUILD_TARGETS {
976 args.push("--target".to_string());
977 args.push(target.to_string());
978 }
979
980 ShellCommand {
981 program: "cmake".to_string(),
982 args,
983 env: vec![],
984 working_dir: Some(PathBuf::from(build_dir)),
985 }
986 }
987
988 fn cmake_install(&self, build_dir: &str) -> ShellCommand {
989 ShellCommand {
990 program: "cmake".to_string(),
991 args: vec!["--install".to_string(), "build".to_string()],
992 env: vec![],
993 working_dir: Some(PathBuf::from(build_dir)),
994 }
995 }
996
997 fn submit_telemetry(
998 &self,
999 home: &str,
1000 build_start: Instant,
1001 was_prebuilt: bool,
1002 binary_path: Option<&std::path::Path>,
1003 ) -> Result<(), String> {
1004 let gpu = crate::hardware::detect_hardware()
1005 .map(|state| state.gpu)
1006 .unwrap_or_default();
1007 let report = BuildReport {
1008 gpu_arch: if gpu.architecture.is_empty() {
1009 self.config.gpu_arch.clone()
1010 } else {
1011 gpu.architecture
1012 },
1013 gpu_count: gpu.gpu_count,
1014 gpu_name: gpu.model,
1015 rocm_version: gpu.rocm_version,
1016 os: std::env::consts::OS.to_string(),
1017 os_distro: std::env::var("ID").unwrap_or_else(|_| "unknown".to_string()),
1018 build_duration_seconds: build_start.elapsed().as_secs(),
1019 git_commit: Self::current_git_commit().unwrap_or_else(|| "unknown".to_string()),
1020 build_status: BuildReportStatus::Success,
1021 install_path: binary_path
1022 .map(|p| p.display().to_string())
1023 .unwrap_or_else(|| {
1024 self.config
1025 .detection_binary_path(home)
1026 .display()
1027 .to_string()
1028 }),
1029 cmake_flags: self.cmake_flags(),
1030 verification_path: self
1031 .config
1032 .detection_binary_path(home)
1033 .display()
1034 .to_string(),
1035 rdna3_validation_passed: false,
1036 wmma_available: false,
1037 shared_memory_ok: false,
1038 tokens_per_second_wmma: None,
1039 tokens_per_second_fallback: None,
1040 binary_version: self.config.branch.clone(),
1041 was_prebuilt,
1042 };
1043 submit_build_report(report);
1044 Ok(())
1045 }
1046
1047 fn current_git_commit() -> Option<String> {
1048 let output = std::process::Command::new("git")
1049 .args(["rev-parse", "HEAD"])
1050 .output()
1051 .ok()?;
1052 if !output.status.success() {
1053 return None;
1054 }
1055 Some(String::from_utf8_lossy(&output.stdout).trim().to_string())
1056 }
1057}
1058
1059pub fn resolve_install_bin_dir(home: &str) -> PathBuf {
1067 PathBuf::from(home).join(DEFAULT_INSTALL_PREFIX).join("bin")
1068}
1069
1070pub fn is_llama_cli_functional(home: &str) -> bool {
1075 let bin_path = resolve_install_bin_dir(home).join(DETECTION_BINARY);
1076 if !bin_path.exists() {
1077 return false;
1078 }
1079 std::process::Command::new(bin_path)
1081 .arg(DETECTION_SUBCOMMAND)
1082 .output()
1083 .map(|o| o.status.success())
1084 .unwrap_or(false)
1085}
1086
1087pub fn is_llama_cli_on_path() -> bool {
1091 std::process::Command::new(DETECTION_BINARY)
1092 .arg(DETECTION_SUBCOMMAND)
1093 .output()
1094 .map(|o| o.status.success())
1095 .unwrap_or(false)
1096}
1097
1098pub fn has_partial_artifacts(home: &str) -> bool {
1103 let build_dir = PathBuf::from(DEFAULT_BUILD_DIR);
1104 let install_dir = PathBuf::from(home).join(DEFAULT_INSTALL_PREFIX);
1105
1106 let artifacts_exist = build_dir.exists() || install_dir.exists();
1107 let binary_functional = is_llama_cli_functional(home);
1108
1109 artifacts_exist && !binary_functional
1110}
1111
1112fn verify_binary_linkage(binary_path: &Path) -> Result<(), String> {
1125 if !binary_path.exists() {
1126 return Err(format!(
1127 "Binary not found at {} — install may have failed",
1128 binary_path.display()
1129 ));
1130 }
1131
1132 let output = std::process::Command::new("ldd")
1133 .arg(binary_path)
1134 .output()
1135 .map_err(|e| format!("Failed to run ldd on {}: {}", binary_path.display(), e))?;
1136
1137 if !output.status.success() {
1138 return Err(format!(
1139 "ldd failed on {}: {}",
1140 binary_path.display(),
1141 String::from_utf8_lossy(&output.stderr)
1142 ));
1143 }
1144
1145 let ldd_output = String::from_utf8_lossy(&output.stdout);
1146
1147 let cuda_markers = ["libcuda.so", "libnvidia", "libcudart"];
1149 for marker in &cuda_markers {
1150 if ldd_output.contains(marker) {
1151 return Err(format!(
1152 "CUDA contamination detected: {} links against '{}'. \
1153 The binary was built with CUDA instead of ROCm/HIP. \
1154 This should not happen — check CMake configuration.",
1155 binary_path.display(),
1156 marker
1157 ));
1158 }
1159 }
1160
1161 let has_rocm = ldd_output.contains("amdhip64")
1163 || ldd_output.contains("hipblas")
1164 || ldd_output.contains("libhip");
1165
1166 if !has_rocm {
1167 return Err(format!(
1168 "ROCm/HIP linkage not found in {}. The binary may be a CPU-only build \
1169 or was built without GPU acceleration. Expected linkage to amdhip64 or hipblas.",
1170 binary_path.display()
1171 ));
1172 }
1173
1174 Ok(())
1175}
1176
1177fn detect_cuda_toolkit_presence() -> Vec<String> {
1181 let mut found = Vec::new();
1182 let cuda_indicators = [
1183 ("nvcc", "NVIDIA CUDA compiler"),
1184 ("nvidia-smi", "NVIDIA system management interface"),
1185 ];
1186
1187 for (binary, description) in cuda_indicators {
1188 if std::process::Command::new("which")
1189 .arg(binary)
1190 .output()
1191 .map(|output| output.status.success())
1192 .unwrap_or(false)
1193 {
1194 found.push(format!("{} ({})", binary, description));
1195 }
1196 }
1197
1198 if Path::new("/usr/local/cuda").exists() {
1199 found.push("/usr/local/cuda directory".to_string());
1200 }
1201
1202 found
1203}
1204
1205fn validate_cmake_cache(cache_path: &Path, expected_gpu_targets: &str) -> Result<(), String> {
1209 let contents = fs::read_to_string(cache_path).map_err(|err| {
1210 format!(
1211 "failed to read CMake cache at {}: {}",
1212 cache_path.display(),
1213 err
1214 )
1215 })?;
1216
1217 let mut hip_on = false;
1218 let mut cuda_off = false;
1219 let mut gpu_targets_seen = false;
1220
1221 for line in contents.lines() {
1222 let trimmed = line.trim();
1223 if trimmed.is_empty() || trimmed.starts_with('#') || trimmed.starts_with("//") {
1224 continue;
1225 }
1226
1227 if trimmed == "GGML_HIP:BOOL=ON" {
1228 hip_on = true;
1229 } else if trimmed == "GGML_CUDA:BOOL=OFF" {
1230 cuda_off = true;
1231 } else if let Some(value) = trimmed.strip_prefix("GPU_TARGETS:STRING=") {
1232 gpu_targets_seen = true;
1233 if value != expected_gpu_targets {
1234 return Err(format!(
1235 "GPU_TARGETS mismatch in CMakeCache.txt: expected '{}', found '{}'",
1236 expected_gpu_targets, value
1237 ));
1238 }
1239 }
1240 }
1241
1242 if !hip_on {
1243 return Err("GGML_HIP is not ON in CMakeCache.txt; build is not using ROCm".to_string());
1244 }
1245 if !cuda_off {
1246 return Err("GGML_CUDA is not OFF in CMakeCache.txt; CUDA contamination risk".to_string());
1247 }
1248 if !gpu_targets_seen {
1249 return Err("GPU_TARGETS is missing from CMakeCache.txt".to_string());
1250 }
1251
1252 Ok(())
1253}
1254
1255#[derive(Debug, Clone, PartialEq)]
1261pub struct PostInstallVerification {
1262 pub help_check_passed: bool,
1264 pub stronger_check_attempted: bool,
1266 pub stronger_check_passed: bool,
1268 pub bench_prefill_tps: Option<f64>,
1270 pub bench_decode_tps: Option<f64>,
1272 pub rdna3_check_attempted: bool,
1274 pub rdna3_is_valid: Option<bool>,
1276 pub rdna3_wmma_tps: Option<f64>,
1278 pub is_rdna3_device: Option<bool>,
1280 pub wmma_supported: Option<bool>,
1282 pub verified_binary_path: String,
1284 pub summary: String,
1286}
1287
1288const VERIFICATION_MODEL_SEARCH_PATHS: &[&str] = &[
1290 "models/Qwen3-0.6B-F16.gguf",
1291 ".mlstack/models/Qwen3-0.6B-F16.gguf",
1292 "llama.cpp-turboquant-hip/models/Qwen3-0.6B-F16.gguf",
1293];
1294
1295fn find_verification_model(home: &str, fork_dir: &str) -> Option<PathBuf> {
1297 for relative in VERIFICATION_MODEL_SEARCH_PATHS {
1298 let candidate = PathBuf::from(home)
1299 .join("Documents/Product/Stan-s-ML-Stack/Fork")
1300 .join(relative.trim_start_matches("llama.cpp-turboquant-hip/"));
1301 if candidate.exists() {
1302 return Some(candidate);
1303 }
1304 }
1305 for relative in VERIFICATION_MODEL_SEARCH_PATHS {
1307 let candidate =
1308 PathBuf::from(fork_dir).join(relative.trim_start_matches("llama.cpp-turboquant-hip/"));
1309 if candidate.exists() {
1310 return Some(candidate);
1311 }
1312 }
1313 None
1314}
1315
1316pub fn verify_installed_binary(home: &str, fork_dir: &str) -> PostInstallVerification {
1322 let bin_dir = resolve_install_bin_dir(home);
1323 let cli_path = bin_dir.join(DETECTION_BINARY);
1324 let bench_path = bin_dir.join("llama-bench");
1325 let rdna3_path = bin_dir.join("rdna3_validation");
1326
1327 let mut result = PostInstallVerification {
1328 help_check_passed: false,
1329 stronger_check_attempted: false,
1330 stronger_check_passed: false,
1331 bench_prefill_tps: None,
1332 bench_decode_tps: None,
1333 rdna3_check_attempted: false,
1334 rdna3_is_valid: None,
1335 rdna3_wmma_tps: None,
1336 is_rdna3_device: None,
1337 wmma_supported: None,
1338 verified_binary_path: cli_path.display().to_string(),
1339 summary: String::new(),
1340 };
1341
1342 if cli_path.exists() {
1344 match std::process::Command::new(&cli_path)
1345 .arg(DETECTION_SUBCOMMAND)
1346 .output()
1347 {
1348 Ok(out) => {
1349 result.help_check_passed = out.status.success();
1350 }
1351 Err(_) => {
1352 result.help_check_passed = false;
1353 }
1354 }
1355 }
1356
1357 if bench_path.exists() {
1359 result.stronger_check_attempted = true;
1360 if let Some(model_path) = find_verification_model(home, fork_dir) {
1361 if let Ok(out) = std::process::Command::new(&bench_path)
1362 .arg("-m")
1363 .arg(model_path.to_str().unwrap_or(""))
1364 .arg("-p")
1365 .arg("128")
1366 .arg("-n")
1367 .arg("32")
1368 .arg("-ngl")
1369 .arg("33")
1370 .output()
1371 {
1372 if out.status.success() {
1373 result.stronger_check_passed = true;
1374 let stdout = String::from_utf8_lossy(&out.stdout);
1376 for line in stdout.lines() {
1377 if line.contains("pp128") {
1378 if let Some(tps) = parse_bench_tps(line) {
1380 result.bench_prefill_tps = Some(tps);
1381 }
1382 } else if line.contains("tg32") {
1383 if let Some(tps) = parse_bench_tps(line) {
1384 result.bench_decode_tps = Some(tps);
1385 }
1386 }
1387 }
1388 }
1389 }
1390 }
1391 }
1392
1393 if rdna3_path.exists() {
1395 result.rdna3_check_attempted = true;
1396 if let Ok(out) = std::process::Command::new(&rdna3_path).output() {
1397 if out.status.success() {
1398 let stdout = String::from_utf8_lossy(&out.stdout);
1399 result.is_rdna3_device = Some(stdout.contains("Is RDNA3: YES"));
1400 result.wmma_supported = Some(stdout.contains("WMMA Supported: YES"));
1401 result.rdna3_is_valid = result.is_rdna3_device;
1402
1403 for line in stdout.lines() {
1405 if line.starts_with("WMMA Throughput:") {
1406 if let Some(val) = line.split_whitespace().nth(2) {
1407 if let Ok(tps) = val.parse::<f64>() {
1408 result.rdna3_wmma_tps = Some(tps);
1409 }
1410 }
1411 }
1412 }
1413 }
1414 }
1415 }
1416
1417 let mut parts: Vec<String> = vec![];
1419 if result.help_check_passed {
1420 parts.push("basic check PASSED".to_string());
1421 } else {
1422 parts.push("basic check FAILED".to_string());
1423 }
1424 if result.stronger_check_attempted {
1425 if result.stronger_check_passed {
1426 parts.push(format!(
1427 "llama-bench PASSED (prefill={:.0} t/s, decode={:.0} t/s)",
1428 result.bench_prefill_tps.unwrap_or(0.0),
1429 result.bench_decode_tps.unwrap_or(0.0)
1430 ));
1431 } else {
1432 parts.push("llama-bench FAILED".to_string());
1433 }
1434 } else {
1435 parts.push("llama-bench SKIPPED (binary unavailable)".to_string());
1436 }
1437 if result.rdna3_check_attempted {
1438 if result.rdna3_is_valid.unwrap_or(false) {
1439 parts.push(format!(
1440 "RDNA3 proof PASSED (WMMA={:.0} t/s)",
1441 result.rdna3_wmma_tps.unwrap_or(0.0)
1442 ));
1443 } else {
1444 parts.push("RDNA3 proof: not an RDNA3 device".to_string());
1445 }
1446 } else {
1447 parts.push("RDNA3 proof SKIPPED (rdna3_validation unavailable)".to_string());
1448 }
1449
1450 result.summary = parts.join("; ");
1451 result
1452}
1453
1454fn parse_bench_tps(line: &str) -> Option<f64> {
1457 let parts: Vec<&str> = line.split('|').collect();
1458 if parts.len() >= 8 {
1459 let tps_cell = parts[7].trim();
1460 tps_cell
1462 .split('±')
1463 .next()
1464 .and_then(|s| s.trim().parse::<f64>().ok())
1465 } else {
1466 None
1467 }
1468}
1469
1470#[cfg(test)]
1475mod tests {
1476 use super::*;
1477
1478 #[test]
1479 fn test_hip_archs_from_gpu_arch() {
1480 assert_eq!(HipArchs::from_gpu_arch("gfx1030"), HipArchs::Gfx1030);
1481 assert_eq!(HipArchs::from_gpu_arch("gfx1100"), HipArchs::Gfx1100);
1482 assert_eq!(HipArchs::from_gpu_arch("gfx1101"), HipArchs::Gfx1100);
1483 assert_eq!(HipArchs::from_gpu_arch("gfx1200"), HipArchs::Gfx1200);
1484 assert_eq!(HipArchs::from_gpu_arch("gfx1201"), HipArchs::Gfx1200);
1485 assert_eq!(HipArchs::from_gpu_arch("gfx000"), HipArchs::Default);
1486 assert_eq!(HipArchs::from_gpu_arch("unknown"), HipArchs::Default);
1487 }
1488
1489 #[test]
1490 fn test_gpu_targets_legacy_channel() {
1491 assert_eq!(
1493 HipArchs::Gfx1100.gpu_targets_for_channel("legacy"),
1494 "gfx1030"
1495 );
1496 assert_eq!(
1497 HipArchs::Gfx1200.gpu_targets_for_channel("legacy"),
1498 "gfx1030"
1499 );
1500 assert_eq!(
1501 HipArchs::Gfx1030.gpu_targets_for_channel("legacy"),
1502 "gfx1030"
1503 );
1504 assert_eq!(
1505 HipArchs::Default.gpu_targets_for_channel("legacy"),
1506 "gfx1030"
1507 );
1508 }
1509
1510 #[test]
1511 fn test_gpu_targets_stable_channel() {
1512 assert_eq!(
1513 HipArchs::Gfx1030.gpu_targets_for_channel("stable"),
1514 "gfx1030"
1515 );
1516 assert_eq!(
1517 HipArchs::Gfx1100.gpu_targets_for_channel("stable"),
1518 "gfx1030;gfx1100;gfx1101"
1519 );
1520 assert_eq!(
1522 HipArchs::Gfx1200.gpu_targets_for_channel("stable"),
1523 "gfx1030;gfx1100;gfx1101"
1524 );
1525 assert_eq!(
1526 HipArchs::Default.gpu_targets_for_channel("stable"),
1527 "gfx1030"
1528 );
1529 }
1530
1531 #[test]
1532 fn test_gpu_targets_latest_channel() {
1533 assert_eq!(
1534 HipArchs::Gfx1030.gpu_targets_for_channel("latest"),
1535 "gfx1030"
1536 );
1537 assert_eq!(
1538 HipArchs::Gfx1100.gpu_targets_for_channel("latest"),
1539 "gfx1030;gfx1100;gfx1101"
1540 );
1541 assert_eq!(
1542 HipArchs::Gfx1200.gpu_targets_for_channel("latest"),
1543 "gfx1030;gfx1100;gfx1101;gfx1200"
1544 );
1545 }
1546
1547 #[test]
1548 fn test_rocm_channel_wmma_fa() {
1549 assert!(!RocmChannel::parse_channel("legacy").enable_wmma_fa());
1550 assert!(RocmChannel::parse_channel("stable").enable_wmma_fa());
1551 assert!(RocmChannel::parse_channel("latest").enable_wmma_fa());
1552 }
1553
1554 #[test]
1555 fn test_cmake_flags_legacy() {
1556 let config = LlamaCppConfig {
1557 gpu_arch: "gfx1100".to_string(),
1558 channel: "legacy".to_string(),
1559 ..Default::default()
1560 };
1561 let installer = LlamaCppInstaller::new(config);
1562 let flags = installer.cmake_flags();
1563
1564 assert!(flags.contains(&"-DGGML_HIP=ON".to_string()));
1565 assert!(flags.contains(&"-DGPU_TARGETS=gfx1030".to_string()));
1566 assert!(!flags.iter().any(|f| f.contains("RDNA3_PROBES")));
1568 assert!(!flags.iter().any(|f| f.contains("ROCWMMA_FATTN")));
1569 }
1570
1571 #[test]
1572 fn test_cmake_flags_always_disable_non_rocm_backends() {
1573 for channel in ["legacy", "stable", "latest"] {
1575 let config = LlamaCppConfig {
1576 gpu_arch: "gfx1100".to_string(),
1577 channel: channel.to_string(),
1578 ..Default::default()
1579 };
1580 let installer = LlamaCppInstaller::new(config);
1581 let flags = installer.cmake_flags();
1582
1583 assert!(
1584 flags.contains(&"-DGGML_CUDA=OFF".to_string()),
1585 "channel {channel} must set GGML_CUDA=OFF"
1586 );
1587 assert!(
1588 flags.contains(&"-DGGML_VULKAN=OFF".to_string()),
1589 "channel {channel} must set GGML_VULKAN=OFF"
1590 );
1591 assert!(
1592 flags.contains(&"-DGGML_METAL=OFF".to_string()),
1593 "channel {channel} must set GGML_METAL=OFF"
1594 );
1595 }
1596 }
1597
1598 #[test]
1599 fn test_cmake_flags_stable() {
1600 let config = LlamaCppConfig {
1601 gpu_arch: "gfx1100".to_string(),
1602 channel: "stable".to_string(),
1603 ..Default::default()
1604 };
1605 let installer = LlamaCppInstaller::new(config);
1606 let flags = installer.cmake_flags();
1607
1608 assert!(flags.contains(&"-DGGML_HIP=ON".to_string()));
1609 assert!(flags.contains(&"-DGPU_TARGETS=gfx1030;gfx1100;gfx1101".to_string()));
1610 assert!(flags.contains(&"-DGGML_HIP_RDNA3_PROBES=ON".to_string()));
1611 assert!(flags.contains(&"-DGGML_HIP_ROCWMMA_FATTN=ON".to_string()));
1612 }
1613
1614 #[test]
1615 fn test_cmake_flags_latest_with_rdna4() {
1616 let config = LlamaCppConfig {
1617 gpu_arch: "gfx1200".to_string(),
1618 channel: "latest".to_string(),
1619 ..Default::default()
1620 };
1621 let installer = LlamaCppInstaller::new(config);
1622 let flags = installer.cmake_flags();
1623
1624 assert!(flags.contains(&"-DGGML_HIP=ON".to_string()));
1625 assert!(flags.contains(&"-DGPU_TARGETS=gfx1030;gfx1100;gfx1101;gfx1200".to_string()));
1626 assert!(flags.contains(&"-DGGML_HIP_RDNA3_PROBES=ON".to_string()));
1627 assert!(flags.contains(&"-DGGML_HIP_ROCWMMA_FATTN=ON".to_string()));
1628 }
1629
1630 #[test]
1631 fn test_rocm_channel_rdna3_gating() {
1632 let legacy = LlamaCppInstaller::new(LlamaCppConfig {
1633 gpu_arch: "gfx1100".to_string(),
1634 channel: "legacy".to_string(),
1635 ..Default::default()
1636 });
1637 let stable = LlamaCppInstaller::new(LlamaCppConfig {
1638 gpu_arch: "gfx1100".to_string(),
1639 channel: "stable".to_string(),
1640 ..Default::default()
1641 });
1642 let latest = LlamaCppInstaller::new(LlamaCppConfig {
1643 gpu_arch: "gfx1100".to_string(),
1644 channel: "latest".to_string(),
1645 ..Default::default()
1646 });
1647
1648 let legacy_flags = legacy.cmake_flags();
1649 let stable_flags = stable.cmake_flags();
1650 let latest_flags = latest.cmake_flags();
1651
1652 assert!(!legacy_flags
1653 .iter()
1654 .any(|flag| flag.contains("RDNA3_PROBES") || flag.contains("ROCWMMA_FATTN")));
1655 assert!(stable_flags.contains(&"-DGGML_HIP_RDNA3_PROBES=ON".to_string()));
1656 assert!(stable_flags.contains(&"-DGGML_HIP_ROCWMMA_FATTN=ON".to_string()));
1657 assert!(latest_flags.contains(&"-DGGML_HIP_RDNA3_PROBES=ON".to_string()));
1658 assert!(latest_flags.contains(&"-DGGML_HIP_ROCWMMA_FATTN=ON".to_string()));
1659 }
1660
1661 #[test]
1662 fn test_cmake_flags_unknown_gpu_conservative() {
1663 let config = LlamaCppConfig {
1664 gpu_arch: "unknown".to_string(),
1665 channel: "latest".to_string(),
1666 ..Default::default()
1667 };
1668 let installer = LlamaCppInstaller::new(config);
1669 let flags = installer.cmake_flags();
1670
1671 assert!(flags.contains(&"-DGPU_TARGETS=gfx1030".to_string()));
1673 }
1674
1675 #[test]
1676 fn test_detect_cuda_toolkit_presence_no_panic() {
1677 let result = detect_cuda_toolkit_presence();
1679 assert!(result.len() <= 3);
1680 }
1681
1682 #[test]
1683 fn test_validate_cmake_cache_accepts_rocm_only_config() {
1684 let temp = tempfile::NamedTempFile::new().unwrap();
1686 fs::write(
1687 temp.path(),
1688 "GGML_HIP:BOOL=ON\nGGML_CUDA:BOOL=OFF\nGPU_TARGETS:STRING=gfx1030;gfx1100;gfx1101\n",
1689 )
1690 .unwrap();
1691
1692 assert!(validate_cmake_cache(temp.path(), "gfx1030;gfx1100;gfx1101").is_ok());
1693 }
1694
1695 #[test]
1696 fn test_validate_cmake_cache_rejects_cuda_on() {
1697 let temp = tempfile::NamedTempFile::new().unwrap();
1699 fs::write(
1700 temp.path(),
1701 "GGML_HIP:BOOL=ON\nGGML_CUDA:BOOL=ON\nGPU_TARGETS:STRING=gfx1030\n",
1702 )
1703 .unwrap();
1704
1705 let err = validate_cmake_cache(temp.path(), "gfx1030").unwrap_err();
1706 assert!(err.contains("GGML_CUDA is not OFF"));
1707 }
1708
1709 #[test]
1710 fn test_validate_cmake_cache_rejects_gpu_target_mismatch() {
1711 let temp = tempfile::NamedTempFile::new().unwrap();
1713 fs::write(
1714 temp.path(),
1715 "GGML_HIP:BOOL=ON\nGGML_CUDA:BOOL=OFF\nGPU_TARGETS:STRING=gfx1030\n",
1716 )
1717 .unwrap();
1718
1719 let err = validate_cmake_cache(temp.path(), "gfx1030;gfx1100;gfx1101").unwrap_err();
1720 assert!(err.contains("GPU_TARGETS mismatch"));
1721 }
1722
1723 #[test]
1724 fn test_build_commands_sequence() {
1725 let config = LlamaCppConfig {
1726 gpu_arch: "gfx1100".to_string(),
1727 channel: "latest".to_string(),
1728 ..Default::default()
1729 };
1730 let installer = LlamaCppInstaller::new(config);
1731 let commands = installer.build_commands("/home/testuser");
1732
1733 assert_eq!(commands.len(), 7);
1735 assert_eq!(commands[0].program, "mkdir");
1736 assert_eq!(commands[1].program, "git");
1737 assert_eq!(commands[2].program, "git");
1738 assert_eq!(commands[3].program, "cmake");
1739 assert_eq!(commands[4].program, "cmake");
1740 assert_eq!(commands[5].program, "cmake");
1741 assert_eq!(commands[6].program, "sh");
1742 }
1743
1744 #[test]
1745 fn test_detection_binary_path() {
1746 let config = LlamaCppConfig::default();
1747 let path = config.detection_binary_path("/home/testuser");
1748 assert_eq!(
1749 path,
1750 PathBuf::from("/home/testuser/.mlstack/components/llama-cpp/bin/llama-cli")
1751 );
1752 }
1753
1754 #[test]
1755 fn test_resolve_install_bin_dir() {
1756 let dir = resolve_install_bin_dir("/home/testuser");
1757 assert_eq!(
1758 dir,
1759 PathBuf::from("/home/testuser/.mlstack/components/llama-cpp/bin")
1760 );
1761 }
1762
1763 #[test]
1764 fn test_is_llama_cli_functional_absent() {
1765 assert!(!is_llama_cli_functional(
1767 "/nonexistent/path/that/does/not/exist"
1768 ));
1769 }
1770
1771 #[test]
1772 fn test_has_partial_artifacts_no_artifacts() {
1773 let _env = crate::test_support::lock_env();
1777 assert!(!has_partial_artifacts(
1781 "/nonexistent-path-DOES-NOT-exist-f94a1f2a3b5c/test-home"
1782 ));
1783 }
1784
1785 #[test]
1786 fn test_dependencies() {
1787 let installer = LlamaCppInstaller::with_defaults();
1788 assert!(installer.dependencies().contains(&"rocm"));
1789 }
1790
1791 #[test]
1792 fn test_repo_url_default() {
1793 let config = LlamaCppConfig::default();
1794 assert_eq!(config.repo_url(), DEFAULT_REPO_URL);
1795 }
1796
1797 #[test]
1798 fn test_repo_url_override() {
1799 let config = LlamaCppConfig {
1800 repo_url: Some("https://example.com/fork.git".to_string()),
1801 ..Default::default()
1802 };
1803 assert_eq!(config.repo_url(), "https://example.com/fork.git");
1804 }
1805
1806 #[test]
1807 fn test_rocm_channel_from_str() {
1808 assert_eq!(RocmChannel::parse_channel("legacy"), RocmChannel::Legacy);
1809 assert_eq!(RocmChannel::parse_channel("LEGACY"), RocmChannel::Legacy);
1810 assert_eq!(RocmChannel::parse_channel("stable"), RocmChannel::Stable);
1811 assert_eq!(RocmChannel::parse_channel("latest"), RocmChannel::Latest);
1812 assert_eq!(RocmChannel::parse_channel("unknown"), RocmChannel::Latest);
1813 }
1814
1815 #[test]
1816 fn test_rocm_channel_label() {
1817 assert_eq!(RocmChannel::Legacy.label(), "legacy");
1818 assert_eq!(RocmChannel::Stable.label(), "stable");
1819 assert_eq!(RocmChannel::Latest.label(), "latest");
1820 }
1821
1822 #[test]
1823 fn test_build_commands_with_auth_token_handling() {
1824 let _original_token = std::env::var("GITHUB_TOKEN").ok();
1826
1827 let config = LlamaCppConfig::default();
1829 let installer = LlamaCppInstaller::new(config);
1830 let commands_with_token = installer
1831 .build_commands_with_auth("/home/testuser", Some(SealedToken::new("mock_token")));
1832
1833 let clone_cmd = &commands_with_token[1];
1835 assert!(!clone_cmd.args.iter().any(|arg| arg.contains("mock_token")));
1837 assert!(clone_cmd.args.iter().any(|arg| arg == DEFAULT_REPO_URL));
1839 assert!(clone_cmd
1841 .args
1842 .iter()
1843 .any(|arg| arg.starts_with("credential.helper=")));
1844 assert!(clone_cmd
1846 .env
1847 .iter()
1848 .any(|(k, v)| k == "GITHUB_TOKEN" && v == "mock_token"));
1849 assert!(clone_cmd
1850 .env
1851 .iter()
1852 .any(|(k, v)| k == "GIT_TERMINAL_PROMPT" && v == "0"));
1853 }
1854
1855 #[test]
1856 fn test_release_manifest_parse_and_lookup() {
1857 let json = r#"{
1858 "version": "v0.3.2",
1859 "archs": {
1860 "gfx1030": {"url": "https://example.com/a", "sha256": "aaa"},
1861 "gfx1100": {"url": "https://example.com/b", "sha256": "bbb"},
1862 "gfx1200": {"url": "https://example.com/c", "sha256": "ccc"}
1863 }
1864 }"#;
1865
1866 let manifest: ReleaseManifest = serde_json::from_str(json).unwrap();
1867 let installer = LlamaCppInstaller::with_defaults();
1868 assert_eq!(manifest.version, "v0.3.2");
1869 assert_eq!(
1870 installer
1871 .release_asset_for_arch(&manifest, "gfx1030")
1872 .unwrap()
1873 .url,
1874 "https://example.com/a"
1875 );
1876 assert_eq!(
1877 installer
1878 .release_asset_for_arch(&manifest, "gfx1100")
1879 .unwrap()
1880 .sha256,
1881 "bbb"
1882 );
1883 assert!(installer
1884 .release_asset_for_arch(&manifest, "gfx9999")
1885 .is_none());
1886 }
1887
1888 #[test]
1889 fn test_release_manifest_rejects_unknown_fields() {
1890 let json = r#"{
1891 "version": "v0.3.2",
1892 "unexpected": true,
1893 "archs": {
1894 "gfx1030": {"url": "https://example.com/a", "sha256": "aaa"},
1895 "gfx1100": {"url": "https://example.com/b", "sha256": "bbb"},
1896 "gfx1200": {"url": "https://example.com/c", "sha256": "ccc"}
1897 }
1898 }"#;
1899
1900 assert!(serde_json::from_str::<ReleaseManifest>(json).is_err());
1901 }
1902
1903 #[test]
1904 fn test_verify_sha256_matches() {
1905 let temp = tempfile::NamedTempFile::new().unwrap();
1906 fs::write(temp.path(), b"hello").unwrap();
1907 let expected = Sha256::digest(b"hello")
1908 .iter()
1909 .map(|byte| format!("{:02x}", byte))
1910 .collect::<String>();
1911 assert!(LlamaCppInstaller::verify_sha256(temp.path(), &expected).unwrap());
1912 }
1913
1914 #[test]
1915 fn test_verify_sha256_mismatch() {
1916 let temp = tempfile::NamedTempFile::new().unwrap();
1917 fs::write(temp.path(), b"hello").unwrap();
1918 assert!(!LlamaCppInstaller::verify_sha256(temp.path(), "deadbeef").unwrap());
1919 }
1920
1921 #[test]
1922 fn test_download_failure_falls_back_to_source() {
1923 let installer = LlamaCppInstaller::with_defaults();
1924 let plan = PrebuiltInstallPlan {
1925 manifest_version: "v1".into(),
1926 arch: "gfx1100".into(),
1927 url: "http://127.0.0.1:9/nope".into(),
1928 sha256: "deadbeef".into(),
1929 install_prefix: tempfile::tempdir().unwrap().path().display().to_string(),
1930 binary_path: PathBuf::from("/tmp/llama-cli"),
1931 };
1932 assert!(installer.download_prebuilt_binary(&plan).is_err());
1933 }
1934
1935 #[test]
1936 fn test_purge_source_artifacts_is_idempotent() {
1937 let _env = crate::test_support::lock_env();
1941 let installer = LlamaCppInstaller::with_defaults();
1942 let temp_home = tempfile::tempdir().unwrap();
1943 let home = temp_home.path().to_string_lossy().to_string();
1944
1945 std::fs::create_dir_all(format!("{home}/.mlstack/components/llama-cpp")).unwrap();
1946 std::fs::create_dir_all(format!("{home}/.mlstack/components/llama-cpp/.git")).unwrap();
1947 std::fs::create_dir_all("/tmp/llama-cpp-rocm-build").unwrap();
1948 std::fs::create_dir_all(format!("{home}/.mlstack/components/llama-cpp/bin")).unwrap();
1949 std::fs::write(
1950 format!("{home}/.mlstack/components/llama-cpp/bin/llama-cli"),
1951 "#!/bin/sh\nexit 0\n",
1952 )
1953 .unwrap();
1954
1955 assert!(installer.purge_source_artifacts(&home, true).is_ok());
1956 assert!(installer.purge_source_artifacts(&home, true).is_ok());
1957
1958 let _ = std::fs::remove_dir_all("/tmp/llama-cpp-rocm-build");
1961 }
1962
1963 #[test]
1964 fn test_git_clone_with_auth() {
1965 let config = LlamaCppConfig::default();
1966 let installer = LlamaCppInstaller::new(config);
1967 let command = installer.git_clone_with_auth("/tmp/test", "mock_token");
1968
1969 assert!(!command.args.iter().any(|arg| arg.contains("mock_token")));
1971 assert!(command.args.iter().any(|arg| arg == DEFAULT_REPO_URL));
1973 assert!(command.args.iter().any(|arg| arg == "-c"));
1975 assert!(command
1976 .args
1977 .iter()
1978 .any(|arg| arg.starts_with("credential.helper=")));
1979 assert!(command
1981 .env
1982 .iter()
1983 .any(|(k, v)| k == "GITHUB_TOKEN" && v == "mock_token"));
1984 assert!(command
1985 .env
1986 .iter()
1987 .any(|(k, v)| k == "GIT_TERMINAL_PROMPT" && v == "0"));
1988 }
1989
1990 #[test]
1996 fn test_post_install_verification_no_binaries() {
1997 let result =
1998 verify_installed_binary("/nonexistent/path/that/does/not/exist", "/nonexistent/fork");
1999 assert!(!result.help_check_passed);
2000 assert!(!result.stronger_check_attempted);
2001 assert!(!result.rdna3_check_attempted);
2002 assert!(!result.summary.is_empty());
2003 assert!(result.summary.contains("FAILED"));
2004 }
2005
2006 #[test]
2007 fn test_post_install_verification_struct_fields() {
2008 let result =
2009 verify_installed_binary("/nonexistent/path/that/does/not/exist", "/nonexistent/fork");
2010 assert!(!result.help_check_passed);
2011 assert!(!result.stronger_check_attempted);
2012 assert!(!result.stronger_check_passed);
2013 assert_eq!(result.bench_prefill_tps, None);
2014 assert_eq!(result.bench_decode_tps, None);
2015 assert!(!result.rdna3_check_attempted);
2016 assert_eq!(result.rdna3_is_valid, None);
2017 assert_eq!(result.rdna3_wmma_tps, None);
2018 assert_eq!(result.is_rdna3_device, None);
2019 assert_eq!(result.wmma_supported, None);
2020 assert!(result.verified_binary_path.contains("llama-cli"));
2021 }
2022
2023 #[test]
2024 fn test_post_install_verification_cli_only() {
2025 let tmp = tempfile::tempdir().unwrap();
2026 let bin_dir = tmp.path().join(".mlstack/components/llama-cpp/bin");
2027 std::fs::create_dir_all(&bin_dir).unwrap();
2028 std::fs::write(
2029 bin_dir.join("llama-cli"),
2030 "#!/bin/sh\necho 'usage: llama-cli'\nexit 0\n",
2031 )
2032 .unwrap();
2033 #[cfg(unix)]
2034 {
2035 use std::os::unix::fs::PermissionsExt;
2036 std::fs::set_permissions(
2037 bin_dir.join("llama-cli"),
2038 std::fs::Permissions::from_mode(0o755),
2039 )
2040 .unwrap();
2041 }
2042
2043 let result = verify_installed_binary(tmp.path().to_str().unwrap(), "/nonexistent/fork");
2044 assert!(result.help_check_passed);
2045 assert!(!result.stronger_check_attempted);
2046 assert!(!result.rdna3_check_attempted);
2047 assert!(result.summary.contains("PASSED"));
2048 assert!(result.summary.contains("SKIPPED"));
2049 }
2050
2051 #[test]
2052 fn test_parse_bench_tps_valid() {
2053 let line =
2054 "| qwen3 0.6B F16 | 1.11 GiB | 596.05 M | ROCm | 33 | pp128 | 6254.94 ± 1534.82 |";
2055 let tps = parse_bench_tps(line);
2056 assert!(tps.is_some());
2057 assert!((tps.unwrap() - 6254.94).abs() < 0.01);
2058 }
2059
2060 #[test]
2061 fn test_parse_bench_tps_no_uncertainty() {
2062 let line = "| qwen3 0.6B F16 | 1.11 GiB | 596.05 M | ROCm | 33 | tg32 | 152.17 |";
2063 let tps = parse_bench_tps(line);
2064 assert!(tps.is_some());
2065 assert!((tps.unwrap() - 152.17).abs() < 0.01);
2066 }
2067
2068 #[test]
2069 fn test_parse_bench_tps_invalid_line() {
2070 assert_eq!(parse_bench_tps("not a bench line"), None);
2071 assert_eq!(parse_bench_tps(""), None);
2072 assert_eq!(parse_bench_tps("| col1 | col2 |"), None);
2073 }
2074
2075 #[test]
2076 fn test_verify_binary_linkage_missing_binary() {
2077 let result = verify_binary_linkage(std::path::Path::new("/nonexistent/binary"));
2079 assert!(result.is_err());
2080 assert!(result.unwrap_err().contains("not found"));
2081 }
2082
2083 #[test]
2084 fn test_verify_binary_linkage_no_panic() {
2085 let result = verify_binary_linkage(std::path::Path::new("/usr/bin/bash"));
2088 assert!(result.is_err());
2090 }
2091}