Skip to main content

llama_cpp_v3/
downloader.rs

1use 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            // Already cached/downloaded
48            return Ok(dll_path);
49        }
50
51        println!(
52            "Downloading llama.cpp {} backend...",
53            backend.release_name_component()
54        );
55
56        // 1. Fetch release metadata
57        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        // Determine OS and Arch string matchers
77        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        // Find the right asset zip
96        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        // 2. Download the ZIP file
118        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        // 3. Extract the DLL from the ZIP
128        {
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        // Clean up the zip file
162        let _ = fs::remove_file(&zip_path);
163
164        println!("DLL path resolved to: {}", dll_path.display());
165        Ok(dll_path)
166    }
167}