llama_cpp_v3/
downloader.rs1use crate::backend::Backend;
2use serde_json::Value;
3use std::fs::{self, File};
4use std::io::{self, BufReader};
5use std::path::PathBuf;
6
7#[derive(Debug, thiserror::Error)]
8pub enum DownloadError {
9 #[error("IO error: {0}")]
10 Io(#[from] io::Error),
11 #[error("Network error: {0}")]
12 Network(String),
13 #[error("Failed to find appropriate release asset for this OS/Backend")]
14 AssetNotFound,
15 #[error("ZIP extraction error: {0}")]
16 Zip(#[from] zip::result::ZipError),
17 #[error("Missing DLL in ZIP")]
18 MissingDll,
19}
20
21pub struct Downloader;
22
23impl Downloader {
24 pub fn ensure_dll(
25 backend: Backend,
26 app_name: &str,
27 version: Option<&str>,
28 cache_dir: Option<PathBuf>,
29 ) -> Result<PathBuf, DownloadError> {
30 let target_dir = if let Some(dir) = cache_dir {
31 dir
32 } else {
33 let mut d = dirs::cache_dir().unwrap_or_else(|| PathBuf::from("."));
34 d.push(app_name);
35 d.push("llama-cpp-v3");
36 d.push(backend.release_name_component());
37 d.push(version.unwrap_or("latest"));
38 d
39 };
40
41 fs::create_dir_all(&target_dir)?;
42
43 let dll_path = target_dir.join(backend.dll_name());
44
45 if dll_path.exists() {
46 println!("DLL path resolved to: {}", dll_path.display());
47 return Ok(dll_path);
49 }
50
51 println!(
52 "Downloading llama.cpp {} backend...",
53 backend.release_name_component()
54 );
55
56 let url = if let Some(v) = version {
58 format!(
59 "https://api.github.com/repos/ggml-org/llama.cpp/releases/tags/{}",
60 v
61 )
62 } else {
63 "https://api.github.com/repos/ggml-org/llama.cpp/releases/latest".to_string()
64 };
65
66 let response = ureq::get(&url)
67 .header("User-Agent", "llama-cpp-v3-rust-wrapper")
68 .call()
69 .map_err(|e| DownloadError::Network(e.to_string()))?;
70
71 let release: Value = response
72 .into_body()
73 .read_json()
74 .map_err(|e| DownloadError::Network(format!("JSON parsing error: {}", e)))?;
75
76 let os_str = if cfg!(windows) {
78 "win"
79 } else if cfg!(target_os = "macos") {
80 "mac"
81 } else {
82 "ubuntu"
83 };
84
85 let arch_str = if cfg!(target_arch = "x86_64") {
86 "x64"
87 } else if cfg!(target_arch = "aarch64") {
88 "arm64"
89 } else {
90 "x86"
91 };
92
93 let backend_str = backend.release_name_component();
94
95 let assets = release["assets"]
97 .as_array()
98 .ok_or(DownloadError::AssetNotFound)?;
99
100 let mut download_url = None;
101 for asset in assets {
102 if let Some(name) = asset["name"].as_str() {
103 let name = name.to_lowercase();
104 if name.ends_with(".zip")
105 && name.contains(os_str)
106 && name.contains(arch_str)
107 && name.contains(backend_str)
108 {
109 download_url = asset["browser_download_url"].as_str().map(String::from);
110 break;
111 }
112 }
113 }
114
115 let download_url = download_url.ok_or(DownloadError::AssetNotFound)?;
116
117 let zip_path = target_dir.join("temp.zip");
119 {
120 let mut file = File::create(&zip_path)?;
121 let mut response = ureq::get(&download_url)
122 .call()
123 .map_err(|e| DownloadError::Network(e.to_string()))?;
124 io::copy(&mut response.body_mut().as_reader(), &mut file)?;
125 }
126
127 {
129 let file = File::open(&zip_path)?;
130 let mut archive = zip::ZipArchive::new(BufReader::new(file))?;
131
132 let mut found = false;
133 for i in 0..archive.len() {
134 let mut file = archive.by_index(i)?;
135 let outpath = match file.enclosed_name() {
136 Some(path) => path.to_owned(),
137 None => continue,
138 };
139
140 if file.is_dir() {
141 let dir_path = target_dir.join(&outpath);
142 fs::create_dir_all(&dir_path)?;
143 } else {
144 if let Some(file_name) = outpath.file_name() {
145 let extracted_path = target_dir.join(file_name);
146 let mut outfile = File::create(&extracted_path)?;
147 io::copy(&mut file, &mut outfile)?;
148
149 if file_name.to_string_lossy() == backend.dll_name() {
150 found = true;
151 }
152 }
153 }
154 }
155
156 if !found {
157 return Err(DownloadError::MissingDll);
158 }
159 }
160
161 let _ = fs::remove_file(&zip_path);
163
164 println!("DLL path resolved to: {}", dll_path.display());
165 Ok(dll_path)
166 }
167}