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 from_str(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::from_str(&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::from_str(&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 match 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 Ok(out) => {
1373 if out.status.success() {
1374 result.stronger_check_passed = true;
1375 let stdout = String::from_utf8_lossy(&out.stdout);
1377 for line in stdout.lines() {
1378 if line.contains("pp128") {
1379 if let Some(tps) = parse_bench_tps(line) {
1381 result.bench_prefill_tps = Some(tps);
1382 }
1383 } else if line.contains("tg32") {
1384 if let Some(tps) = parse_bench_tps(line) {
1385 result.bench_decode_tps = Some(tps);
1386 }
1387 }
1388 }
1389 }
1390 }
1391 Err(_) => {}
1392 }
1393 }
1394 }
1395
1396 if rdna3_path.exists() {
1398 result.rdna3_check_attempted = true;
1399 match std::process::Command::new(&rdna3_path).output() {
1400 Ok(out) => {
1401 if out.status.success() {
1402 let stdout = String::from_utf8_lossy(&out.stdout);
1403 result.is_rdna3_device = Some(stdout.contains("Is RDNA3: YES"));
1404 result.wmma_supported = Some(stdout.contains("WMMA Supported: YES"));
1405 result.rdna3_is_valid = result.is_rdna3_device;
1406
1407 for line in stdout.lines() {
1409 if line.starts_with("WMMA Throughput:") {
1410 if let Some(val) = line.split_whitespace().nth(2) {
1411 if let Ok(tps) = val.parse::<f64>() {
1412 result.rdna3_wmma_tps = Some(tps);
1413 }
1414 }
1415 }
1416 }
1417 }
1418 }
1419 Err(_) => {}
1420 }
1421 }
1422
1423 let mut parts: Vec<String> = vec![];
1425 if result.help_check_passed {
1426 parts.push("basic check PASSED".to_string());
1427 } else {
1428 parts.push("basic check FAILED".to_string());
1429 }
1430 if result.stronger_check_attempted {
1431 if result.stronger_check_passed {
1432 parts.push(format!(
1433 "llama-bench PASSED (prefill={:.0} t/s, decode={:.0} t/s)",
1434 result.bench_prefill_tps.unwrap_or(0.0),
1435 result.bench_decode_tps.unwrap_or(0.0)
1436 ));
1437 } else {
1438 parts.push("llama-bench FAILED".to_string());
1439 }
1440 } else {
1441 parts.push("llama-bench SKIPPED (binary unavailable)".to_string());
1442 }
1443 if result.rdna3_check_attempted {
1444 if result.rdna3_is_valid.unwrap_or(false) {
1445 parts.push(format!(
1446 "RDNA3 proof PASSED (WMMA={:.0} t/s)",
1447 result.rdna3_wmma_tps.unwrap_or(0.0)
1448 ));
1449 } else {
1450 parts.push("RDNA3 proof: not an RDNA3 device".to_string());
1451 }
1452 } else {
1453 parts.push("RDNA3 proof SKIPPED (rdna3_validation unavailable)".to_string());
1454 }
1455
1456 result.summary = parts.join("; ");
1457 result
1458}
1459
1460fn parse_bench_tps(line: &str) -> Option<f64> {
1463 let parts: Vec<&str> = line.split('|').collect();
1464 if parts.len() >= 8 {
1465 let tps_cell = parts[7].trim();
1466 tps_cell
1468 .split('±')
1469 .next()
1470 .and_then(|s| s.trim().parse::<f64>().ok())
1471 } else {
1472 None
1473 }
1474}
1475
1476#[cfg(test)]
1481mod tests {
1482 use super::*;
1483
1484 #[test]
1485 fn test_hip_archs_from_gpu_arch() {
1486 assert_eq!(HipArchs::from_gpu_arch("gfx1030"), HipArchs::Gfx1030);
1487 assert_eq!(HipArchs::from_gpu_arch("gfx1100"), HipArchs::Gfx1100);
1488 assert_eq!(HipArchs::from_gpu_arch("gfx1101"), HipArchs::Gfx1100);
1489 assert_eq!(HipArchs::from_gpu_arch("gfx1200"), HipArchs::Gfx1200);
1490 assert_eq!(HipArchs::from_gpu_arch("gfx1201"), HipArchs::Gfx1200);
1491 assert_eq!(HipArchs::from_gpu_arch("gfx000"), HipArchs::Default);
1492 assert_eq!(HipArchs::from_gpu_arch("unknown"), HipArchs::Default);
1493 }
1494
1495 #[test]
1496 fn test_gpu_targets_legacy_channel() {
1497 assert_eq!(
1499 HipArchs::Gfx1100.gpu_targets_for_channel("legacy"),
1500 "gfx1030"
1501 );
1502 assert_eq!(
1503 HipArchs::Gfx1200.gpu_targets_for_channel("legacy"),
1504 "gfx1030"
1505 );
1506 assert_eq!(
1507 HipArchs::Gfx1030.gpu_targets_for_channel("legacy"),
1508 "gfx1030"
1509 );
1510 assert_eq!(
1511 HipArchs::Default.gpu_targets_for_channel("legacy"),
1512 "gfx1030"
1513 );
1514 }
1515
1516 #[test]
1517 fn test_gpu_targets_stable_channel() {
1518 assert_eq!(
1519 HipArchs::Gfx1030.gpu_targets_for_channel("stable"),
1520 "gfx1030"
1521 );
1522 assert_eq!(
1523 HipArchs::Gfx1100.gpu_targets_for_channel("stable"),
1524 "gfx1030;gfx1100;gfx1101"
1525 );
1526 assert_eq!(
1528 HipArchs::Gfx1200.gpu_targets_for_channel("stable"),
1529 "gfx1030;gfx1100;gfx1101"
1530 );
1531 assert_eq!(
1532 HipArchs::Default.gpu_targets_for_channel("stable"),
1533 "gfx1030"
1534 );
1535 }
1536
1537 #[test]
1538 fn test_gpu_targets_latest_channel() {
1539 assert_eq!(
1540 HipArchs::Gfx1030.gpu_targets_for_channel("latest"),
1541 "gfx1030"
1542 );
1543 assert_eq!(
1544 HipArchs::Gfx1100.gpu_targets_for_channel("latest"),
1545 "gfx1030;gfx1100;gfx1101"
1546 );
1547 assert_eq!(
1548 HipArchs::Gfx1200.gpu_targets_for_channel("latest"),
1549 "gfx1030;gfx1100;gfx1101;gfx1200"
1550 );
1551 }
1552
1553 #[test]
1554 fn test_rocm_channel_wmma_fa() {
1555 assert!(!RocmChannel::from_str("legacy").enable_wmma_fa());
1556 assert!(RocmChannel::from_str("stable").enable_wmma_fa());
1557 assert!(RocmChannel::from_str("latest").enable_wmma_fa());
1558 }
1559
1560 #[test]
1561 fn test_cmake_flags_legacy() {
1562 let config = LlamaCppConfig {
1563 gpu_arch: "gfx1100".to_string(),
1564 channel: "legacy".to_string(),
1565 ..Default::default()
1566 };
1567 let installer = LlamaCppInstaller::new(config);
1568 let flags = installer.cmake_flags();
1569
1570 assert!(flags.contains(&"-DGGML_HIP=ON".to_string()));
1571 assert!(flags.contains(&"-DGPU_TARGETS=gfx1030".to_string()));
1572 assert!(!flags.iter().any(|f| f.contains("RDNA3_PROBES")));
1574 assert!(!flags.iter().any(|f| f.contains("ROCWMMA_FATTN")));
1575 }
1576
1577 #[test]
1578 fn test_cmake_flags_always_disable_non_rocm_backends() {
1579 for channel in ["legacy", "stable", "latest"] {
1581 let config = LlamaCppConfig {
1582 gpu_arch: "gfx1100".to_string(),
1583 channel: channel.to_string(),
1584 ..Default::default()
1585 };
1586 let installer = LlamaCppInstaller::new(config);
1587 let flags = installer.cmake_flags();
1588
1589 assert!(
1590 flags.contains(&"-DGGML_CUDA=OFF".to_string()),
1591 "channel {channel} must set GGML_CUDA=OFF"
1592 );
1593 assert!(
1594 flags.contains(&"-DGGML_VULKAN=OFF".to_string()),
1595 "channel {channel} must set GGML_VULKAN=OFF"
1596 );
1597 assert!(
1598 flags.contains(&"-DGGML_METAL=OFF".to_string()),
1599 "channel {channel} must set GGML_METAL=OFF"
1600 );
1601 }
1602 }
1603
1604 #[test]
1605 fn test_cmake_flags_stable() {
1606 let config = LlamaCppConfig {
1607 gpu_arch: "gfx1100".to_string(),
1608 channel: "stable".to_string(),
1609 ..Default::default()
1610 };
1611 let installer = LlamaCppInstaller::new(config);
1612 let flags = installer.cmake_flags();
1613
1614 assert!(flags.contains(&"-DGGML_HIP=ON".to_string()));
1615 assert!(flags.contains(&"-DGPU_TARGETS=gfx1030;gfx1100;gfx1101".to_string()));
1616 assert!(flags.contains(&"-DGGML_HIP_RDNA3_PROBES=ON".to_string()));
1617 assert!(flags.contains(&"-DGGML_HIP_ROCWMMA_FATTN=ON".to_string()));
1618 }
1619
1620 #[test]
1621 fn test_cmake_flags_latest_with_rdna4() {
1622 let config = LlamaCppConfig {
1623 gpu_arch: "gfx1200".to_string(),
1624 channel: "latest".to_string(),
1625 ..Default::default()
1626 };
1627 let installer = LlamaCppInstaller::new(config);
1628 let flags = installer.cmake_flags();
1629
1630 assert!(flags.contains(&"-DGGML_HIP=ON".to_string()));
1631 assert!(flags.contains(&"-DGPU_TARGETS=gfx1030;gfx1100;gfx1101;gfx1200".to_string()));
1632 assert!(flags.contains(&"-DGGML_HIP_RDNA3_PROBES=ON".to_string()));
1633 assert!(flags.contains(&"-DGGML_HIP_ROCWMMA_FATTN=ON".to_string()));
1634 }
1635
1636 #[test]
1637 fn test_rocm_channel_rdna3_gating() {
1638 let legacy = LlamaCppInstaller::new(LlamaCppConfig {
1639 gpu_arch: "gfx1100".to_string(),
1640 channel: "legacy".to_string(),
1641 ..Default::default()
1642 });
1643 let stable = LlamaCppInstaller::new(LlamaCppConfig {
1644 gpu_arch: "gfx1100".to_string(),
1645 channel: "stable".to_string(),
1646 ..Default::default()
1647 });
1648 let latest = LlamaCppInstaller::new(LlamaCppConfig {
1649 gpu_arch: "gfx1100".to_string(),
1650 channel: "latest".to_string(),
1651 ..Default::default()
1652 });
1653
1654 let legacy_flags = legacy.cmake_flags();
1655 let stable_flags = stable.cmake_flags();
1656 let latest_flags = latest.cmake_flags();
1657
1658 assert!(!legacy_flags
1659 .iter()
1660 .any(|flag| flag.contains("RDNA3_PROBES") || flag.contains("ROCWMMA_FATTN")));
1661 assert!(stable_flags.contains(&"-DGGML_HIP_RDNA3_PROBES=ON".to_string()));
1662 assert!(stable_flags.contains(&"-DGGML_HIP_ROCWMMA_FATTN=ON".to_string()));
1663 assert!(latest_flags.contains(&"-DGGML_HIP_RDNA3_PROBES=ON".to_string()));
1664 assert!(latest_flags.contains(&"-DGGML_HIP_ROCWMMA_FATTN=ON".to_string()));
1665 }
1666
1667 #[test]
1668 fn test_cmake_flags_unknown_gpu_conservative() {
1669 let config = LlamaCppConfig {
1670 gpu_arch: "unknown".to_string(),
1671 channel: "latest".to_string(),
1672 ..Default::default()
1673 };
1674 let installer = LlamaCppInstaller::new(config);
1675 let flags = installer.cmake_flags();
1676
1677 assert!(flags.contains(&"-DGPU_TARGETS=gfx1030".to_string()));
1679 }
1680
1681 #[test]
1682 fn test_detect_cuda_toolkit_presence_no_panic() {
1683 let result = detect_cuda_toolkit_presence();
1685 assert!(result.len() <= 3);
1686 }
1687
1688 #[test]
1689 fn test_validate_cmake_cache_accepts_rocm_only_config() {
1690 let temp = tempfile::NamedTempFile::new().unwrap();
1692 fs::write(
1693 temp.path(),
1694 "GGML_HIP:BOOL=ON\nGGML_CUDA:BOOL=OFF\nGPU_TARGETS:STRING=gfx1030;gfx1100;gfx1101\n",
1695 )
1696 .unwrap();
1697
1698 assert!(validate_cmake_cache(temp.path(), "gfx1030;gfx1100;gfx1101").is_ok());
1699 }
1700
1701 #[test]
1702 fn test_validate_cmake_cache_rejects_cuda_on() {
1703 let temp = tempfile::NamedTempFile::new().unwrap();
1705 fs::write(
1706 temp.path(),
1707 "GGML_HIP:BOOL=ON\nGGML_CUDA:BOOL=ON\nGPU_TARGETS:STRING=gfx1030\n",
1708 )
1709 .unwrap();
1710
1711 let err = validate_cmake_cache(temp.path(), "gfx1030").unwrap_err();
1712 assert!(err.contains("GGML_CUDA is not OFF"));
1713 }
1714
1715 #[test]
1716 fn test_validate_cmake_cache_rejects_gpu_target_mismatch() {
1717 let temp = tempfile::NamedTempFile::new().unwrap();
1719 fs::write(
1720 temp.path(),
1721 "GGML_HIP:BOOL=ON\nGGML_CUDA:BOOL=OFF\nGPU_TARGETS:STRING=gfx1030\n",
1722 )
1723 .unwrap();
1724
1725 let err = validate_cmake_cache(temp.path(), "gfx1030;gfx1100;gfx1101").unwrap_err();
1726 assert!(err.contains("GPU_TARGETS mismatch"));
1727 }
1728
1729 #[test]
1730 fn test_build_commands_sequence() {
1731 let config = LlamaCppConfig {
1732 gpu_arch: "gfx1100".to_string(),
1733 channel: "latest".to_string(),
1734 ..Default::default()
1735 };
1736 let installer = LlamaCppInstaller::new(config);
1737 let commands = installer.build_commands("/home/testuser");
1738
1739 assert_eq!(commands.len(), 7);
1741 assert_eq!(commands[0].program, "mkdir");
1742 assert_eq!(commands[1].program, "git");
1743 assert_eq!(commands[2].program, "git");
1744 assert_eq!(commands[3].program, "cmake");
1745 assert_eq!(commands[4].program, "cmake");
1746 assert_eq!(commands[5].program, "cmake");
1747 assert_eq!(commands[6].program, "sh");
1748 }
1749
1750 #[test]
1751 fn test_detection_binary_path() {
1752 let config = LlamaCppConfig::default();
1753 let path = config.detection_binary_path("/home/testuser");
1754 assert_eq!(
1755 path,
1756 PathBuf::from("/home/testuser/.mlstack/components/llama-cpp/bin/llama-cli")
1757 );
1758 }
1759
1760 #[test]
1761 fn test_resolve_install_bin_dir() {
1762 let dir = resolve_install_bin_dir("/home/testuser");
1763 assert_eq!(
1764 dir,
1765 PathBuf::from("/home/testuser/.mlstack/components/llama-cpp/bin")
1766 );
1767 }
1768
1769 #[test]
1770 fn test_is_llama_cli_functional_absent() {
1771 assert!(!is_llama_cli_functional(
1773 "/nonexistent/path/that/does/not/exist"
1774 ));
1775 }
1776
1777 #[test]
1778 fn test_has_partial_artifacts_no_artifacts() {
1779 assert!(!has_partial_artifacts(
1783 "/nonexistent-path-DOES-NOT-exist-f94a1f2a3b5c/test-home"
1784 ));
1785 }
1786
1787 #[test]
1788 fn test_dependencies() {
1789 let installer = LlamaCppInstaller::with_defaults();
1790 assert!(installer.dependencies().contains(&"rocm"));
1791 }
1792
1793 #[test]
1794 fn test_repo_url_default() {
1795 let config = LlamaCppConfig::default();
1796 assert_eq!(config.repo_url(), DEFAULT_REPO_URL);
1797 }
1798
1799 #[test]
1800 fn test_repo_url_override() {
1801 let config = LlamaCppConfig {
1802 repo_url: Some("https://example.com/fork.git".to_string()),
1803 ..Default::default()
1804 };
1805 assert_eq!(config.repo_url(), "https://example.com/fork.git");
1806 }
1807
1808 #[test]
1809 fn test_rocm_channel_from_str() {
1810 assert_eq!(RocmChannel::from_str("legacy"), RocmChannel::Legacy);
1811 assert_eq!(RocmChannel::from_str("LEGACY"), RocmChannel::Legacy);
1812 assert_eq!(RocmChannel::from_str("stable"), RocmChannel::Stable);
1813 assert_eq!(RocmChannel::from_str("latest"), RocmChannel::Latest);
1814 assert_eq!(RocmChannel::from_str("unknown"), RocmChannel::Latest);
1815 }
1816
1817 #[test]
1818 fn test_rocm_channel_label() {
1819 assert_eq!(RocmChannel::Legacy.label(), "legacy");
1820 assert_eq!(RocmChannel::Stable.label(), "stable");
1821 assert_eq!(RocmChannel::Latest.label(), "latest");
1822 }
1823
1824 #[test]
1825 fn test_build_commands_with_auth_token_handling() {
1826 let _original_token = std::env::var("GITHUB_TOKEN").ok();
1828
1829 let config = LlamaCppConfig::default();
1831 let installer = LlamaCppInstaller::new(config);
1832 let commands_with_token = installer
1833 .build_commands_with_auth("/home/testuser", Some(SealedToken::new("mock_token")));
1834
1835 let clone_cmd = &commands_with_token[1];
1837 assert!(!clone_cmd.args.iter().any(|arg| arg.contains("mock_token")));
1839 assert!(clone_cmd.args.iter().any(|arg| arg == DEFAULT_REPO_URL));
1841 assert!(clone_cmd
1843 .args
1844 .iter()
1845 .any(|arg| arg.starts_with("credential.helper=")));
1846 assert!(clone_cmd
1848 .env
1849 .iter()
1850 .any(|(k, v)| k == "GITHUB_TOKEN" && v == "mock_token"));
1851 assert!(clone_cmd
1852 .env
1853 .iter()
1854 .any(|(k, v)| k == "GIT_TERMINAL_PROMPT" && v == "0"));
1855 }
1856
1857 #[test]
1858 fn test_release_manifest_parse_and_lookup() {
1859 let json = r#"{
1860 "version": "v0.3.2",
1861 "archs": {
1862 "gfx1030": {"url": "https://example.com/a", "sha256": "aaa"},
1863 "gfx1100": {"url": "https://example.com/b", "sha256": "bbb"},
1864 "gfx1200": {"url": "https://example.com/c", "sha256": "ccc"}
1865 }
1866 }"#;
1867
1868 let manifest: ReleaseManifest = serde_json::from_str(json).unwrap();
1869 let installer = LlamaCppInstaller::with_defaults();
1870 assert_eq!(manifest.version, "v0.3.2");
1871 assert_eq!(
1872 installer
1873 .release_asset_for_arch(&manifest, "gfx1030")
1874 .unwrap()
1875 .url,
1876 "https://example.com/a"
1877 );
1878 assert_eq!(
1879 installer
1880 .release_asset_for_arch(&manifest, "gfx1100")
1881 .unwrap()
1882 .sha256,
1883 "bbb"
1884 );
1885 assert!(installer
1886 .release_asset_for_arch(&manifest, "gfx9999")
1887 .is_none());
1888 }
1889
1890 #[test]
1891 fn test_release_manifest_rejects_unknown_fields() {
1892 let json = r#"{
1893 "version": "v0.3.2",
1894 "unexpected": true,
1895 "archs": {
1896 "gfx1030": {"url": "https://example.com/a", "sha256": "aaa"},
1897 "gfx1100": {"url": "https://example.com/b", "sha256": "bbb"},
1898 "gfx1200": {"url": "https://example.com/c", "sha256": "ccc"}
1899 }
1900 }"#;
1901
1902 assert!(serde_json::from_str::<ReleaseManifest>(json).is_err());
1903 }
1904
1905 #[test]
1906 fn test_verify_sha256_matches() {
1907 let temp = tempfile::NamedTempFile::new().unwrap();
1908 fs::write(temp.path(), b"hello").unwrap();
1909 let expected = Sha256::digest(b"hello")
1910 .iter()
1911 .map(|byte| format!("{:02x}", byte))
1912 .collect::<String>();
1913 assert!(LlamaCppInstaller::verify_sha256(temp.path(), &expected).unwrap());
1914 }
1915
1916 #[test]
1917 fn test_verify_sha256_mismatch() {
1918 let temp = tempfile::NamedTempFile::new().unwrap();
1919 fs::write(temp.path(), b"hello").unwrap();
1920 assert!(!LlamaCppInstaller::verify_sha256(temp.path(), "deadbeef").unwrap());
1921 }
1922
1923 #[test]
1924 fn test_download_failure_falls_back_to_source() {
1925 let installer = LlamaCppInstaller::with_defaults();
1926 let plan = PrebuiltInstallPlan {
1927 manifest_version: "v1".into(),
1928 arch: "gfx1100".into(),
1929 url: "http://127.0.0.1:9/nope".into(),
1930 sha256: "deadbeef".into(),
1931 install_prefix: tempfile::tempdir().unwrap().path().display().to_string(),
1932 binary_path: PathBuf::from("/tmp/llama-cli"),
1933 };
1934 assert!(installer.download_prebuilt_binary(&plan).is_err());
1935 }
1936
1937 #[test]
1938 fn test_purge_source_artifacts_is_idempotent() {
1939 let installer = LlamaCppInstaller::with_defaults();
1940 let temp_home = tempfile::tempdir().unwrap();
1941 let home = temp_home.path().to_string_lossy().to_string();
1942
1943 std::fs::create_dir_all(format!("{home}/.mlstack/components/llama-cpp")).unwrap();
1944 std::fs::create_dir_all(format!("{home}/.mlstack/components/llama-cpp/.git")).unwrap();
1945 std::fs::create_dir_all("/tmp/llama-cpp-rocm-build").unwrap();
1946 std::fs::create_dir_all(format!("{home}/.mlstack/components/llama-cpp/bin")).unwrap();
1947 std::fs::write(
1948 format!("{home}/.mlstack/components/llama-cpp/bin/llama-cli"),
1949 "#!/bin/sh\nexit 0\n",
1950 )
1951 .unwrap();
1952
1953 assert!(installer.purge_source_artifacts(&home, true).is_ok());
1954 assert!(installer.purge_source_artifacts(&home, true).is_ok());
1955 }
1956
1957 #[test]
1958 fn test_git_clone_with_auth() {
1959 let config = LlamaCppConfig::default();
1960 let installer = LlamaCppInstaller::new(config);
1961 let command = installer.git_clone_with_auth("/tmp/test", "mock_token");
1962
1963 assert!(!command.args.iter().any(|arg| arg.contains("mock_token")));
1965 assert!(command.args.iter().any(|arg| arg == DEFAULT_REPO_URL));
1967 assert!(command.args.iter().any(|arg| arg == "-c"));
1969 assert!(command
1970 .args
1971 .iter()
1972 .any(|arg| arg.starts_with("credential.helper=")));
1973 assert!(command
1975 .env
1976 .iter()
1977 .any(|(k, v)| k == "GITHUB_TOKEN" && v == "mock_token"));
1978 assert!(command
1979 .env
1980 .iter()
1981 .any(|(k, v)| k == "GIT_TERMINAL_PROMPT" && v == "0"));
1982 }
1983
1984 #[test]
1990 fn test_post_install_verification_no_binaries() {
1991 let result =
1992 verify_installed_binary("/nonexistent/path/that/does/not/exist", "/nonexistent/fork");
1993 assert!(!result.help_check_passed);
1994 assert!(!result.stronger_check_attempted);
1995 assert!(!result.rdna3_check_attempted);
1996 assert!(!result.summary.is_empty());
1997 assert!(result.summary.contains("FAILED"));
1998 }
1999
2000 #[test]
2001 fn test_post_install_verification_struct_fields() {
2002 let result =
2003 verify_installed_binary("/nonexistent/path/that/does/not/exist", "/nonexistent/fork");
2004 assert!(!result.help_check_passed);
2005 assert!(!result.stronger_check_attempted);
2006 assert!(!result.stronger_check_passed);
2007 assert_eq!(result.bench_prefill_tps, None);
2008 assert_eq!(result.bench_decode_tps, None);
2009 assert!(!result.rdna3_check_attempted);
2010 assert_eq!(result.rdna3_is_valid, None);
2011 assert_eq!(result.rdna3_wmma_tps, None);
2012 assert_eq!(result.is_rdna3_device, None);
2013 assert_eq!(result.wmma_supported, None);
2014 assert!(result.verified_binary_path.contains("llama-cli"));
2015 }
2016
2017 #[test]
2018 fn test_post_install_verification_cli_only() {
2019 let tmp = tempfile::tempdir().unwrap();
2020 let bin_dir = tmp.path().join(".mlstack/components/llama-cpp/bin");
2021 std::fs::create_dir_all(&bin_dir).unwrap();
2022 std::fs::write(
2023 bin_dir.join("llama-cli"),
2024 "#!/bin/sh\necho 'usage: llama-cli'\nexit 0\n",
2025 )
2026 .unwrap();
2027 #[cfg(unix)]
2028 {
2029 use std::os::unix::fs::PermissionsExt;
2030 std::fs::set_permissions(
2031 bin_dir.join("llama-cli"),
2032 std::fs::Permissions::from_mode(0o755),
2033 )
2034 .unwrap();
2035 }
2036
2037 let result = verify_installed_binary(tmp.path().to_str().unwrap(), "/nonexistent/fork");
2038 assert!(result.help_check_passed);
2039 assert!(!result.stronger_check_attempted);
2040 assert!(!result.rdna3_check_attempted);
2041 assert!(result.summary.contains("PASSED"));
2042 assert!(result.summary.contains("SKIPPED"));
2043 }
2044
2045 #[test]
2046 fn test_parse_bench_tps_valid() {
2047 let line =
2048 "| qwen3 0.6B F16 | 1.11 GiB | 596.05 M | ROCm | 33 | pp128 | 6254.94 ± 1534.82 |";
2049 let tps = parse_bench_tps(line);
2050 assert!(tps.is_some());
2051 assert!((tps.unwrap() - 6254.94).abs() < 0.01);
2052 }
2053
2054 #[test]
2055 fn test_parse_bench_tps_no_uncertainty() {
2056 let line = "| qwen3 0.6B F16 | 1.11 GiB | 596.05 M | ROCm | 33 | tg32 | 152.17 |";
2057 let tps = parse_bench_tps(line);
2058 assert!(tps.is_some());
2059 assert!((tps.unwrap() - 152.17).abs() < 0.01);
2060 }
2061
2062 #[test]
2063 fn test_parse_bench_tps_invalid_line() {
2064 assert_eq!(parse_bench_tps("not a bench line"), None);
2065 assert_eq!(parse_bench_tps(""), None);
2066 assert_eq!(parse_bench_tps("| col1 | col2 |"), None);
2067 }
2068
2069 #[test]
2070 fn test_verify_binary_linkage_missing_binary() {
2071 let result = verify_binary_linkage(std::path::Path::new("/nonexistent/binary"));
2073 assert!(result.is_err());
2074 assert!(result.unwrap_err().contains("not found"));
2075 }
2076
2077 #[test]
2078 fn test_verify_binary_linkage_no_panic() {
2079 let result = verify_binary_linkage(std::path::Path::new("/usr/bin/bash"));
2082 assert!(result.is_err());
2084 }
2085}