use std::path::{Path, PathBuf};
use std::time::Duration;
use anyhow::{Context, Result, bail};
use futures_util::StreamExt;
use serde::Deserialize;
use sha2::{Digest, Sha256};
use tokio::io::AsyncWriteExt;
use crate::shared::config::{AppConfig, ServerMode};
use crate::shared::i18n::Locale;
const RELEASES_API: &str = "https://api.github.com/repos/ggml-org/llama.cpp/releases";
const USER_AGENT: &str = "mindfork-llama-setup";
const RELEASE_SCAN: usize = 5;
const CONNECT_TIMEOUT: Duration = Duration::from_secs(10);
const READ_TIMEOUT: Duration = Duration::from_secs(60);
const PROGRESS_STEP: u64 = 32 * 1024 * 1024;
const PROBE_TIMEOUT: Duration = Duration::from_secs(60);
#[derive(Debug, Clone, Deserialize)]
pub struct Asset {
pub name: String,
pub size: u64,
#[serde(default)]
pub digest: Option<String>,
pub browser_download_url: String,
}
#[derive(Debug, Clone, Deserialize)]
pub struct Release {
pub tag_name: String,
#[serde(default)]
pub published_at: Option<String>,
#[serde(default)]
pub assets: Vec<Asset>,
}
#[derive(Debug, Clone)]
pub struct Backend {
pub id: String,
pub asset: Asset,
pub cudart: Option<Asset>,
}
impl Backend {
pub fn download_size(&self) -> u64 {
self.asset.size + self.cudart.as_ref().map_or(0, |c| c.size)
}
pub fn cudart_missing(&self) -> bool {
self.needs_cudart() && self.cudart.is_none()
}
pub fn needs_cudart(&self) -> bool {
self.id.starts_with("cuda-")
}
}
#[derive(Debug, Clone)]
pub struct Listing {
pub tag: String,
pub date: String,
pub backends: Vec<Backend>,
}
#[derive(Debug, Clone)]
pub struct Install {
pub backend: String,
pub tag: String,
pub dir: PathBuf,
pub bytes: u64,
pub binary_ok: bool,
}
#[derive(Debug, Clone)]
pub struct SetupOptions {
pub backend: String,
pub build: Option<String>,
pub force: bool,
pub cudart: bool,
}
#[allow(dead_code)]
#[derive(Debug, Clone)]
pub struct Installed {
pub backend: String,
pub tag: String,
pub dir: PathBuf,
pub binary: PathBuf,
pub version: String,
pub devices: Vec<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Ext {
Zip,
TarGz,
}
impl Ext {
fn of(name: &str) -> Option<Self> {
if let Some(rest) = name.strip_suffix(".tar.gz") {
(!rest.is_empty()).then_some(Ext::TarGz)
} else if let Some(rest) = name.strip_suffix(".zip") {
(!rest.is_empty()).then_some(Ext::Zip)
} else {
None
}
}
fn strip(self, name: &str) -> &str {
let suffix = match self {
Ext::Zip => ".zip",
Ext::TarGz => ".tar.gz",
};
name.strip_suffix(suffix).unwrap_or(name)
}
}
pub fn os_token(os: &str) -> Option<&'static str> {
match os {
"windows" => Some("win"),
"linux" => Some("ubuntu"),
"macos" => Some("macos"),
_ => None,
}
}
pub fn arch_token(arch: &str) -> Option<&'static str> {
match arch {
"x86_64" => Some("x64"),
"aarch64" => Some("arm64"),
_ => None,
}
}
fn backend_of(name: &str, tag: &str, os_tok: &str, arch_tok: &str) -> Option<(String, Ext)> {
let ext = Ext::of(name)?;
let stem = ext.strip(name);
let rest = stem.strip_prefix(&format!("llama-{tag}-bin-"))?;
let mut parts = rest.split('-');
if parts.next()? != os_tok {
return None;
}
let middle: Vec<&str> = parts.collect();
let (&last, head) = middle.split_last()?;
if last != arch_tok {
return None;
}
let id = if head.is_empty() {
"cpu".to_string()
} else {
head.join("-")
};
(!id.is_empty()).then_some((id, ext))
}
fn cudart_name(backend: &str, os_tok: &str, arch_tok: &str) -> String {
format!("cudart-llama-bin-{os_tok}-{backend}-{arch_tok}.zip")
}
pub fn backends(release: &Release, os: &str, arch: &str) -> Vec<Backend> {
let (Some(os_tok), Some(arch_tok)) = (os_token(os), arch_token(arch)) else {
return Vec::new();
};
let mut out: Vec<Backend> = Vec::new();
for asset in &release.assets {
let Some((id, _ext)) = backend_of(&asset.name, &release.tag_name, os_tok, arch_tok) else {
continue;
};
if out.iter().any(|b| b.id == id) {
continue; }
let cudart = id.starts_with("cuda-").then(|| {
let want = cudart_name(&id, os_tok, arch_tok);
release.assets.iter().find(|a| a.name == want).cloned()
});
out.push(Backend {
id,
asset: asset.clone(),
cudart: cudart.flatten(),
});
}
out.sort_by(|a, b| a.id.cmp(&b.id));
out
}
pub fn install_name(backend: &str, tag: &str) -> String {
format!("{backend}-{tag}")
}
pub fn split_install_name(name: &str) -> Option<(&str, &str)> {
let (backend, tag) = name.rsplit_once('-')?;
(!backend.is_empty() && !tag.is_empty()).then_some((backend, tag))
}
pub fn server_binary_name() -> &'static str {
if cfg!(windows) {
"llama-server.exe"
} else {
"llama-server"
}
}
fn digest_hex(digest: &str) -> Option<&str> {
let hex = digest.strip_prefix("sha256:")?;
(hex.len() == 64 && hex.bytes().all(|b| b.is_ascii_hexdigit())).then_some(hex)
}
pub fn tag_build_number(tag: &str) -> Option<u64> {
tag.strip_prefix('b')?.parse::<u64>().ok()
}
pub fn version_build_number(text: &str) -> Option<u64> {
let at = text.find("build ")? + "build ".len();
let digits: String = text[at..]
.chars()
.take_while(char::is_ascii_digit)
.collect();
digits.parse::<u64>().ok()
}
pub fn parse_devices(stdout: &str) -> Vec<String> {
stdout
.lines()
.skip(1)
.map(str::trim)
.filter(|l| !l.is_empty() && *l != "(none)")
.map(str::to_string)
.collect()
}
fn http_client(loc: &Locale) -> Result<reqwest::Client> {
reqwest::Client::builder()
.user_agent(USER_AGENT)
.connect_timeout(CONNECT_TIMEOUT)
.read_timeout(READ_TIMEOUT)
.build()
.with_context(|| loc.t("llamacpp.setup.http_client").to_string())
}
async fn api_get(client: &reqwest::Client, url: &str, loc: &Locale) -> Result<String> {
let resp = client
.get(url)
.header("Accept", "application/vnd.github+json")
.send()
.await
.with_context(|| loc.tf("llamacpp.setup.request", &[("url", url)]))?;
let status = resp.status();
if status == reqwest::StatusCode::FORBIDDEN || status == reqwest::StatusCode::TOO_MANY_REQUESTS
{
let remaining = resp
.headers()
.get("x-ratelimit-remaining")
.and_then(|v| v.to_str().ok())
.unwrap_or("");
if remaining == "0" {
bail!("{}", loc.t("llamacpp.setup.rate_limited"));
}
}
resp.error_for_status()
.with_context(|| loc.tf("llamacpp.setup.download", &[("url", url)]))?
.text()
.await
.with_context(|| loc.t("llamacpp.setup.read_body").to_string())
}
async fn fetch_release(
client: &reqwest::Client,
build: Option<&str>,
os: &str,
arch: &str,
loc: &Locale,
) -> Result<Release> {
if let Some(tag) = build {
let url = format!("{RELEASES_API}/tags/{tag}");
let body = api_get(client, &url, loc).await?;
let release: Release = serde_json::from_str(&body)
.with_context(|| loc.tf("llamacpp.setup.release_parse", &[("tag", tag)]))?;
return Ok(release);
}
let url = format!("{RELEASES_API}?per_page={RELEASE_SCAN}");
let body = api_get(client, &url, loc).await?;
let releases: Vec<Release> = serde_json::from_str(&body)
.with_context(|| loc.t("llamacpp.setup.releases_parse").to_string())?;
releases
.into_iter()
.find(|r| !backends(r, os, arch).is_empty())
.ok_or_else(|| {
anyhow::anyhow!(
"{}",
loc.tf(
"llamacpp.setup.no_release",
&[
("count", &RELEASE_SCAN.to_string()),
("os", os),
("arch", arch)
],
)
)
})
}
fn check_platform(os: &str, arch: &str, loc: &Locale) -> Result<()> {
if os_token(os).is_some() && arch_token(arch).is_some() {
return Ok(());
}
bail!(
"{}",
loc.tf("llamacpp.setup.no_platform", &[("os", os), ("arch", arch)])
)
}
pub async fn list_backends(build: Option<&str>, loc: &Locale) -> Result<Listing> {
let (os, arch) = (std::env::consts::OS, std::env::consts::ARCH);
check_platform(os, arch, loc)?;
let client = http_client(loc)?;
let release = fetch_release(&client, build, os, arch, loc).await?;
let backends = backends(&release, os, arch);
if backends.is_empty() {
bail!(
"{}",
loc.tf(
"llamacpp.setup.no_backends",
&[("tag", &release.tag_name), ("os", os), ("arch", arch)],
)
);
}
Ok(Listing {
date: release
.published_at
.as_deref()
.and_then(|p| p.split('T').next())
.unwrap_or("")
.to_string(),
tag: release.tag_name,
backends,
})
}
pub fn render_backends(listing: &Listing, root: &Path, loc: &Locale) -> Vec<String> {
let (os, arch) = (std::env::consts::OS, std::env::consts::ARCH);
let mut lines = vec![loc.tf(
"llamacpp.backends.header",
&[
("tag", &listing.tag),
("date", &listing.date),
("os", os),
("arch", arch),
],
)];
let width = listing
.backends
.iter()
.map(|b| b.id.chars().count())
.max()
.unwrap_or(0);
for b in &listing.backends {
let mut notes: Vec<String> = Vec::new();
if b.cudart.is_some() {
notes.push(loc.t("llamacpp.backends.with_cudart").to_string());
} else if b.needs_cudart() {
notes.push(loc.t("llamacpp.backends.no_cudart").to_string());
}
if root.join(install_name(&b.id, &listing.tag)).is_dir() {
notes.push(loc.t("llamacpp.backends.installed").to_string());
}
let note = if notes.is_empty() {
String::new()
} else {
format!(" ({})", notes.join(", "))
};
lines.push(format!(
" {:width$} {:>5} MB{}",
b.id,
mib(b.download_size()),
note,
width = width
));
}
lines
}
fn mib(bytes: u64) -> u64 {
bytes >> 20
}
pub async fn setup(
root: &Path,
opts: &SetupOptions,
loc: &Locale,
mut progress: impl FnMut(&str),
) -> Result<Installed> {
let (os, arch) = (std::env::consts::OS, std::env::consts::ARCH);
check_platform(os, arch, loc)?;
tokio::fs::create_dir_all(root).await.with_context(|| {
loc.tf(
"llamacpp.setup.mkdir",
&[("path", &root.display().to_string())],
)
})?;
let client = http_client(loc)?;
let release = fetch_release(&client, opts.build.as_deref(), os, arch, loc).await?;
let available = backends(&release, os, arch);
let backend = available
.iter()
.find(|b| b.id == opts.backend)
.ok_or_else(|| {
let list = available
.iter()
.map(|b| b.id.as_str())
.collect::<Vec<_>>()
.join(", ");
anyhow::anyhow!(
"{}",
loc.tf(
"llamacpp.setup.unknown_backend",
&[
("id", &opts.backend),
("tag", &release.tag_name),
("os", os),
("arch", arch),
("list", &list),
],
)
)
})?;
if opts.cudart && backend.cudart_missing() {
bail!(
"{}",
loc.tf(
"llamacpp.setup.cudart_missing",
&[
("id", &backend.id),
("tag", &release.tag_name),
(
"name",
&cudart_name(
&backend.id,
os_token(os).unwrap_or(""),
arch_token(arch).unwrap_or("")
)
),
],
)
);
}
let name = install_name(&backend.id, &release.tag_name);
let dir = root.join(&name);
let staging = root.join(format!(".tmp-{name}"));
if dir.is_dir() && !opts.force {
progress(&loc.tf(
"llamacpp.setup.present",
&[("path", &dir.display().to_string())],
));
return probe(&backend.id, &release.tag_name, &dir, loc, &mut progress).await;
}
if opts.force {
let _ = tokio::fs::remove_dir_all(&staging).await;
}
tokio::fs::create_dir_all(&staging).await.with_context(|| {
loc.tf(
"llamacpp.setup.mkdir",
&[("path", &staging.display().to_string())],
)
})?;
let mut wanted: Vec<&Asset> = vec![&backend.asset];
if opts.cudart
&& let Some(c) = &backend.cudart
{
wanted.push(c);
}
let total: u64 = wanted.iter().map(|a| a.size).sum();
progress(&loc.tf(
"llamacpp.setup.downloading",
&[
("tag", &release.tag_name),
("backend", &backend.id),
("os", os),
("arch", arch),
("size", &mib(total).to_string()),
],
));
let unpacked = staging.join("unpacked");
let _ = tokio::fs::remove_dir_all(&unpacked).await;
for (i, asset) in wanted.iter().enumerate() {
let archive = staging.join(&asset.name);
fetch_verified(&client, asset, &archive, loc, &mut progress).await?;
progress(&loc.tf("llamacpp.setup.extracting", &[("name", &asset.name)]));
let raw = staging.join(format!("raw{i}"));
let _ = tokio::fs::remove_dir_all(&raw).await;
extract(&archive, &asset.name, &raw, loc)?;
merge_payload(&raw, &unpacked, loc)?;
let _ = tokio::fs::remove_dir_all(&raw).await;
}
let staged_binary = unpacked.join(server_binary_name());
if !staged_binary.is_file() {
bail!(
"{}",
loc.tf(
"llamacpp.setup.binary_missing",
&[
("name", server_binary_name()),
("path", &unpacked.display().to_string()),
],
)
);
}
let version = first_line(&run_probe(&staged_binary, &["--version"], loc).await?);
if let (Some(want), Some(got)) = (
tag_build_number(&release.tag_name),
version_build_number(&version),
) && want != got
{
bail!(
"{}",
loc.tf(
"llamacpp.setup.build_mismatch",
&[("got", &got.to_string()), ("expected", &want.to_string())],
)
);
}
if dir.exists() {
tokio::fs::remove_dir_all(&dir).await.with_context(|| {
loc.tf(
"llamacpp.setup.remove_dir",
&[("path", &dir.display().to_string())],
)
})?;
}
tokio::fs::rename(&unpacked, &dir).await.with_context(|| {
loc.tf(
"llamacpp.setup.rename",
&[("path", &dir.display().to_string())],
)
})?;
let _ = tokio::fs::remove_dir_all(&staging).await;
progress(&loc.tf(
"llamacpp.setup.installed",
&[("path", &dir.display().to_string())],
));
probe(&backend.id, &release.tag_name, &dir, loc, &mut progress).await
}
async fn probe(
backend: &str,
tag: &str,
dir: &Path,
loc: &Locale,
progress: &mut impl FnMut(&str),
) -> Result<Installed> {
let binary = dir.join(server_binary_name());
if !binary.is_file() {
bail!(
"{}",
loc.tf(
"llamacpp.setup.binary_missing",
&[
("name", server_binary_name()),
("path", &dir.display().to_string()),
],
)
);
}
let version = first_line(&run_probe(&binary, &["--version"], loc).await?);
let devices = parse_devices(&run_probe(&binary, &["--list-devices"], loc).await?);
progress(&loc.tf("llamacpp.setup.version", &[("version", &version)]));
if devices.is_empty() {
if backend != "cpu" {
progress(&loc.tf("llamacpp.setup.no_devices", &[("backend", backend)]));
}
} else {
progress(&loc.tf(
"llamacpp.setup.devices",
&[("devices", &devices.join("; "))],
));
}
Ok(Installed {
backend: backend.to_string(),
tag: tag.to_string(),
dir: dir.to_path_buf(),
binary,
version,
devices,
})
}
fn first_line(text: &str) -> String {
text.lines()
.map(str::trim)
.find(|l| !l.is_empty())
.unwrap_or("")
.to_string()
}
async fn run_probe(binary: &Path, args: &[&str], loc: &Locale) -> Result<String> {
let run = tokio::process::Command::new(binary)
.args(args)
.current_dir(binary.parent().unwrap_or(Path::new(".")))
.output();
let out = tokio::time::timeout(PROBE_TIMEOUT, run)
.await
.map_err(|_| {
anyhow::anyhow!(
"{}",
loc.tf(
"llamacpp.setup.probe_timeout",
&[("path", &binary.display().to_string())],
)
)
})?
.with_context(|| {
loc.tf(
"llamacpp.setup.probe_failed",
&[("path", &binary.display().to_string())],
)
})?;
let stdout = String::from_utf8_lossy(&out.stdout).to_string();
let stderr = String::from_utf8_lossy(&out.stderr).to_string();
if stdout.trim().is_empty() {
Ok(stderr)
} else {
Ok(stdout)
}
}
pub fn installed(root: &Path) -> Vec<Install> {
let Ok(entries) = std::fs::read_dir(root) else {
return Vec::new();
};
let mut out: Vec<Install> = Vec::new();
for entry in entries.flatten() {
let path = entry.path();
if !path.is_dir() {
continue;
}
let Some(name) = path.file_name().and_then(std::ffi::OsStr::to_str) else {
continue;
};
if name.starts_with('.') {
continue;
}
let Some((backend, tag)) = split_install_name(name) else {
continue;
};
out.push(Install {
backend: backend.to_string(),
tag: tag.to_string(),
bytes: dir_size(&path),
binary_ok: path.join(server_binary_name()).is_file(),
dir: path,
});
}
out.sort_by(|a, b| (&a.backend, &a.tag).cmp(&(&b.backend, &b.tag)));
out
}
pub fn render_installed(installs: &[Install], root: &Path, loc: &Locale) -> Vec<String> {
if installs.is_empty() {
return vec![loc.t("llamacpp.installed.none").to_string()];
}
let mut lines = vec![loc.tf(
"llamacpp.installed.header",
&[("path", &root.display().to_string())],
)];
let name_of = |i: &Install| {
i.dir
.file_name()
.map(|n| n.to_string_lossy().to_string())
.unwrap_or_else(|| install_name(&i.backend, &i.tag))
};
let width = installs
.iter()
.map(|i| name_of(i).chars().count())
.max()
.unwrap_or(0);
for i in installs {
let broken = if i.binary_ok {
String::new()
} else {
format!(" ({})", loc.t("llamacpp.installed.broken"))
};
lines.push(format!(
" {:width$} {:>5} MB{}",
name_of(i),
mib(i.bytes),
broken,
width = width
));
}
lines
}
fn dir_size(dir: &Path) -> u64 {
let Ok(entries) = std::fs::read_dir(dir) else {
return 0;
};
let mut total = 0;
for entry in entries.flatten() {
match entry.metadata() {
Ok(m) if m.is_dir() => total += dir_size(&entry.path()),
Ok(m) => total += m.len(),
Err(_) => {}
}
}
total
}
pub fn newest_install(root: &Path) -> Option<PathBuf> {
let Ok(entries) = std::fs::read_dir(root) else {
return None;
};
let mut best: Option<(std::time::SystemTime, PathBuf)> = None;
for entry in entries.flatten() {
let dir = entry.path();
let Some(name) = dir.file_name().and_then(std::ffi::OsStr::to_str) else {
continue;
};
if name.starts_with('.') || split_install_name(name).is_none() {
continue;
}
let binary = dir.join(server_binary_name());
if !binary.is_file() {
continue; }
let Ok(meta) = entry.metadata() else { continue };
let Ok(when) = meta.created().or_else(|_| meta.modified()) else {
continue;
};
if best.as_ref().is_none_or(|(best_when, _)| when > *best_when) {
best = Some((when, binary));
}
}
best.map(|(_, binary)| binary)
}
pub fn resolve_binary(
configured: Option<&str>,
exe_dir: Option<&Path>,
llama_dir: Option<&Path>,
) -> Option<PathBuf> {
let beside = |dir: Option<&Path>, name: &str| -> Option<PathBuf> {
let candidate = dir?.join(name);
candidate.is_file().then_some(candidate)
};
match configured.map(str::trim).filter(|s| !s.is_empty()) {
Some(path) => {
if Path::new(path)
.parent()
.is_some_and(|p| !p.as_os_str().is_empty())
{
return Some(PathBuf::from(path)); }
Some(beside(exe_dir, path).unwrap_or_else(|| PathBuf::from(path)))
}
None => llama_dir
.and_then(newest_install)
.or_else(|| beside(exe_dir, server_binary_name())),
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct BinaryTargets {
pub assistant: bool,
pub impersonation: bool,
pub embed: bool,
pub mode_is_managed: bool,
}
pub fn set_engine_binary(config: &mut AppConfig, binary: &Path) -> BinaryTargets {
let path = binary.display().to_string();
let vacant = |v: &Option<String>| v.as_ref().is_none_or(|s| s.trim().is_empty());
config.engine.managed.binary = Some(path.clone());
let impersonation = vacant(&config.impersonation_engine.managed.binary);
if impersonation {
config.impersonation_engine.managed.binary = Some(path.clone());
}
let embed = vacant(&config.embed.managed.binary);
if embed {
config.embed.managed.binary = Some(path);
}
BinaryTargets {
assistant: true,
impersonation,
embed,
mode_is_managed: config.engine.mode == ServerMode::Managed,
}
}
pub fn render_binary_targets(t: &BinaryTargets, binary: &Path, loc: &Locale) -> Vec<String> {
let mut lines = vec![loc.tf(
"llamacpp.setbinary.assistant",
&[("path", &binary.display().to_string())],
)];
let mut also: Vec<&str> = Vec::new();
if t.impersonation {
also.push(loc.t("llamacpp.setbinary.impersonation"));
}
if t.embed {
also.push(loc.t("llamacpp.setbinary.embed"));
}
if !also.is_empty() {
lines.push(loc.tf("llamacpp.setbinary.also", &[("what", &also.join(", "))]));
}
if !t.mode_is_managed {
lines.push(loc.t("llamacpp.setbinary.not_managed").to_string());
}
lines
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BinaryUse {
Assistant,
Impersonation,
Embed,
}
impl BinaryUse {
fn key(self) -> &'static str {
match self {
BinaryUse::Assistant => "llamacpp.setbinary.assistant_name",
BinaryUse::Impersonation => "llamacpp.setbinary.impersonation",
BinaryUse::Embed => "llamacpp.setbinary.embed",
}
}
}
pub fn find_install(root: &Path, id: &str) -> Option<Install> {
let all = installed(root);
if let Some(exact) = all
.iter()
.find(|i| i.dir.file_name().and_then(std::ffi::OsStr::to_str) == Some(id))
{
return Some(exact.clone());
}
let mut by_backend = all.iter().filter(|i| i.backend == id);
let only = by_backend.next()?;
by_backend.next().is_none().then(|| only.clone())
}
pub fn binary_uses(config: &AppConfig, dir: &Path) -> Vec<BinaryUse> {
[
(BinaryUse::Assistant, &config.engine.managed.binary),
(
BinaryUse::Impersonation,
&config.impersonation_engine.managed.binary,
),
(BinaryUse::Embed, &config.embed.managed.binary),
]
.into_iter()
.filter(|(_, configured)| points_inside(configured.as_deref(), dir))
.map(|(use_, _)| use_)
.collect()
}
fn points_inside(configured: Option<&str>, dir: &Path) -> bool {
let Some(configured) = configured.map(str::trim).filter(|s| !s.is_empty()) else {
return false;
};
let configured = Path::new(configured);
if configured.starts_with(dir) {
return true;
}
match (
std::fs::canonicalize(configured),
std::fs::canonicalize(dir),
) {
(Ok(binary), Ok(dir)) => binary.starts_with(dir),
_ => false,
}
}
pub fn remove_install(dir: &Path, loc: &Locale) -> Result<()> {
std::fs::remove_dir_all(dir).with_context(|| {
loc.tf(
"llamacpp.setup.remove_dir",
&[("path", &dir.display().to_string())],
)
})
}
pub fn render_removed(
install: &Install,
root: &Path,
exe_dir: Option<&Path>,
loc: &Locale,
) -> Vec<String> {
let mut lines = vec![loc.tf(
"llamacpp.remove.done",
&[
("path", &install.dir.display().to_string()),
("size", &mib(install.bytes).to_string()),
],
)];
match resolve_binary(None, exe_dir, Some(root)) {
Some(next) => lines.push(loc.tf(
"llamacpp.remove.now_resolves",
&[("path", &next.display().to_string())],
)),
None => lines.push(loc.t("llamacpp.remove.nothing_left").to_string()),
}
lines
}
pub fn render_in_use(uses: &[BinaryUse], install: &Install, loc: &Locale) -> String {
let names: Vec<&str> = uses.iter().map(|u| loc.t(u.key())).collect();
loc.tf(
"llamacpp.remove.in_use",
&[
("id", &install_name(&install.backend, &install.tag)),
("what", &names.join(", ")),
],
)
}
async fn fetch_verified(
client: &reqwest::Client,
asset: &Asset,
dest: &Path,
loc: &Locale,
progress: &mut impl FnMut(&str),
) -> Result<()> {
let expected = asset
.digest
.as_deref()
.and_then(digest_hex)
.ok_or_else(|| {
anyhow::anyhow!(
"{}",
loc.tf("llamacpp.setup.no_digest", &[("name", &asset.name)])
)
})?;
if dest.is_file()
&& tokio::fs::metadata(dest).await.map(|m| m.len()).ok() == Some(asset.size)
&& verify_file(dest, expected, &asset.name, loc).await.is_ok()
{
progress(&loc.tf("llamacpp.setup.asset_present", &[("name", &asset.name)]));
return Ok(());
}
let part = PathBuf::from(format!("{}.part", dest.display()));
let resumable = part.is_file();
let first = stream_to_part(client, asset, &part, resumable, expected, loc, progress).await;
if let Err(err) = first {
if !resumable {
return Err(err);
}
progress(&loc.tf("llamacpp.setup.retry", &[("name", &asset.name)]));
let _ = tokio::fs::remove_file(&part).await;
stream_to_part(client, asset, &part, false, expected, loc, progress).await?;
}
let _ = tokio::fs::remove_file(dest).await;
tokio::fs::rename(&part, dest).await.with_context(|| {
loc.tf(
"llamacpp.setup.rename",
&[("path", &dest.display().to_string())],
)
})
}
async fn stream_to_part(
client: &reqwest::Client,
asset: &Asset,
part: &Path,
resume: bool,
expected: &str,
loc: &Locale,
progress: &mut impl FnMut(&str),
) -> Result<()> {
let have = if resume {
tokio::fs::metadata(part)
.await
.map(|m| m.len())
.unwrap_or(0)
} else {
0
};
let have = if have >= asset.size { 0 } else { have };
let mut req = client.get(&asset.browser_download_url);
if have > 0 {
req = req.header(reqwest::header::RANGE, format!("bytes={have}-"));
}
let resp = req
.send()
.await
.with_context(|| {
loc.tf(
"llamacpp.setup.request",
&[("url", &asset.browser_download_url)],
)
})?
.error_for_status()
.with_context(|| {
loc.tf(
"llamacpp.setup.download",
&[("url", &asset.browser_download_url)],
)
})?;
let partial = resp.status() == reqwest::StatusCode::PARTIAL_CONTENT;
let (mut hasher, mut done, append) = if have > 0 && partial {
progress(&loc.tf(
"llamacpp.setup.resuming",
&[("done", &mib(have).to_string())],
));
(hash_of(part, loc).await?, have, true)
} else {
(Sha256::new(), 0, false)
};
let mut file = tokio::fs::OpenOptions::new()
.create(true)
.write(true)
.append(append)
.truncate(!append)
.open(part)
.await
.with_context(|| {
loc.tf(
"llamacpp.setup.create_file",
&[("path", &part.display().to_string())],
)
})?;
let mut next_report = done + PROGRESS_STEP;
let mut stream = resp.bytes_stream();
while let Some(chunk) = stream.next().await {
let chunk = chunk.with_context(|| loc.t("llamacpp.setup.read_stream").to_string())?;
hasher.update(&chunk);
file.write_all(&chunk)
.await
.with_context(|| loc.t("llamacpp.setup.write_file").to_string())?;
done += chunk.len() as u64;
if done >= next_report {
progress(&loc.tf(
"llamacpp.setup.progress_bytes",
&[
("done", &mib(done).to_string()),
("total", &mib(asset.size).to_string()),
],
));
next_report = done + PROGRESS_STEP;
}
}
file.flush()
.await
.with_context(|| loc.t("llamacpp.setup.flush").to_string())?;
verify_sha256(&hasher.finalize(), expected, &asset.name, loc)
}
async fn hash_of(path: &Path, loc: &Locale) -> Result<Sha256> {
use tokio::io::AsyncReadExt;
let mut file = tokio::fs::File::open(path).await.with_context(|| {
loc.tf(
"llamacpp.setup.open_file",
&[("path", &path.display().to_string())],
)
})?;
let mut hasher = Sha256::new();
let mut buf = vec![0u8; 64 * 1024];
loop {
let read = file
.read(&mut buf)
.await
.with_context(|| loc.t("llamacpp.setup.read_file").to_string())?;
if read == 0 {
break;
}
hasher.update(&buf[..read]);
}
Ok(hasher)
}
async fn verify_file(path: &Path, expected: &str, label: &str, loc: &Locale) -> Result<()> {
let hasher = hash_of(path, loc).await?;
verify_sha256(&hasher.finalize(), expected, label, loc)
}
fn verify_sha256(digest: &[u8], expected: &str, label: &str, loc: &Locale) -> Result<()> {
let got = hex_lower(digest);
if got.eq_ignore_ascii_case(expected) {
Ok(())
} else {
bail!(
"{}",
loc.tf(
"llamacpp.setup.sha_mismatch",
&[("name", label), ("expected", expected), ("got", &got)],
)
)
}
}
fn hex_lower(bytes: &[u8]) -> String {
use std::fmt::Write;
let mut s = String::with_capacity(bytes.len() * 2);
for b in bytes {
let _ = write!(s, "{b:02x}");
}
s
}
fn extract(archive: &Path, name: &str, dest: &Path, loc: &Locale) -> Result<()> {
std::fs::create_dir_all(dest).with_context(|| {
loc.tf(
"llamacpp.setup.mkdir",
&[("path", &dest.display().to_string())],
)
})?;
match Ext::of(name) {
Some(Ext::TarGz) => extract_targz(archive, dest, loc),
Some(Ext::Zip) => extract_zip(archive, dest, loc),
None => bail!(
"{}",
loc.tf("llamacpp.setup.unknown_archive", &[("name", name)])
),
}
}
fn extract_targz(archive: &Path, dest: &Path, loc: &Locale) -> Result<()> {
let file = std::fs::File::open(archive).with_context(|| {
loc.tf(
"llamacpp.setup.open_archive",
&[("path", &archive.display().to_string())],
)
})?;
let gz = flate2::read::GzDecoder::new(std::io::BufReader::new(file));
tar::Archive::new(gz).unpack(dest).with_context(|| {
loc.tf(
"llamacpp.setup.extract_to",
&[("path", &dest.display().to_string())],
)
})
}
fn extract_zip(archive: &Path, dest: &Path, loc: &Locale) -> Result<()> {
let file = std::fs::File::open(archive).with_context(|| {
loc.tf(
"llamacpp.setup.open_archive",
&[("path", &archive.display().to_string())],
)
})?;
let mut zip = zip::ZipArchive::new(std::io::BufReader::new(file)).with_context(|| {
loc.tf(
"llamacpp.setup.open_archive",
&[("path", &archive.display().to_string())],
)
})?;
for i in 0..zip.len() {
let mut entry = zip.by_index(i)?;
let Some(rel) = entry.enclosed_name() else {
bail!(
"{}",
loc.tf("llamacpp.setup.unsafe_entry", &[("name", entry.name())])
);
};
let out = dest.join(rel);
if entry.is_dir() {
std::fs::create_dir_all(&out)?;
continue;
}
if let Some(parent) = out.parent() {
std::fs::create_dir_all(parent)?;
}
let mut out_file = std::fs::File::create(&out).with_context(|| {
loc.tf(
"llamacpp.setup.create_file",
&[("path", &out.display().to_string())],
)
})?;
std::io::copy(&mut entry, &mut out_file)?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let mode = entry.unix_mode().unwrap_or(0o755);
let _ = std::fs::set_permissions(&out, std::fs::Permissions::from_mode(mode));
}
}
Ok(())
}
fn merge_payload(raw: &Path, dest: &Path, loc: &Locale) -> Result<()> {
let payload = single_child_dir(raw).unwrap_or_else(|| raw.to_path_buf());
std::fs::create_dir_all(dest).with_context(|| {
loc.tf(
"llamacpp.setup.mkdir",
&[("path", &dest.display().to_string())],
)
})?;
let entries = std::fs::read_dir(&payload).with_context(|| {
loc.tf(
"llamacpp.setup.read_dir",
&[("path", &payload.display().to_string())],
)
})?;
for entry in entries.flatten() {
let to = dest.join(entry.file_name());
let _ = std::fs::remove_file(&to);
std::fs::rename(entry.path(), &to).with_context(|| {
loc.tf(
"llamacpp.setup.rename",
&[("path", &to.display().to_string())],
)
})?;
}
Ok(())
}
fn single_child_dir(dir: &Path) -> Option<PathBuf> {
let mut only: Option<PathBuf> = None;
for entry in std::fs::read_dir(dir).ok()?.flatten() {
if only.is_some() || !entry.path().is_dir() {
return None;
}
only = Some(entry.path());
}
only
}
#[cfg(test)]
mod tests {
use super::*;
use crate::shared::i18n::{Lang, locale};
const B10883: &[&str] = &[
"cudart-llama-bin-win-cuda-12.4-x64.zip",
"cudart-llama-bin-win-cuda-13.3-x64.zip",
"cudart-llama-bin-win-cuda-13.4-arm64.zip",
"llama-b10883-bin-android-arm64.tar.gz",
"llama-b10883-bin-macos-arm64.tar.gz",
"llama-b10883-bin-macos-x64.tar.gz",
"llama-b10883-bin-ubuntu-arm64.tar.gz",
"llama-b10883-bin-ubuntu-openvino-2026.3.1-x64.tar.gz",
"llama-b10883-bin-ubuntu-rocm-10.0-x64.tar.gz",
"llama-b10883-bin-ubuntu-s390x.tar.gz",
"llama-b10883-bin-ubuntu-sycl-fp16-x64.tar.gz",
"llama-b10883-bin-ubuntu-sycl-fp32-x64.tar.gz",
"llama-b10883-bin-ubuntu-vulkan-arm64.tar.gz",
"llama-b10883-bin-ubuntu-vulkan-x64.tar.gz",
"llama-b10883-bin-ubuntu-x64.tar.gz",
"llama-b10883-bin-win-cpu-arm64.zip",
"llama-b10883-bin-win-cpu-x64.zip",
"llama-b10883-bin-win-cuda-12.4-x64.zip",
"llama-b10883-bin-win-cuda-13.3-x64.zip",
"llama-b10883-bin-win-cuda-13.4-arm64.zip",
"llama-b10883-bin-win-opencl-adreno-arm64.zip",
"llama-b10883-bin-win-openvino-2026.3.1-x64.zip",
"llama-b10883-bin-win-rocm-10.0-x64.zip",
"llama-b10883-bin-win-sycl-x64.zip",
"llama-b10883-bin-win-vulkan-x64.zip",
"llama-b10883-ui.tar.gz",
"llama-b10883-xcframework.zip",
];
const B9000: &[&str] = &[
"cudart-llama-bin-win-cuda-12.4-x64.zip",
"cudart-llama-bin-win-cuda-13.1-x64.zip",
"llama-b9000-bin-310p-openEuler-aarch64.tar.gz",
"llama-b9000-bin-310p-openEuler-x86.tar.gz",
"llama-b9000-bin-910b-openEuler-aarch64-aclgraph.tar.gz",
"llama-b9000-bin-910b-openEuler-x86-aclgraph.tar.gz",
"llama-b9000-bin-macos-arm64-kleidiai.tar.gz",
"llama-b9000-bin-macos-arm64.tar.gz",
"llama-b9000-bin-ubuntu-x64.tar.gz",
"llama-b9000-bin-win-cpu-x64.zip",
"llama-b9000-bin-win-cuda-12.4-x64.zip",
"llama-b9000-bin-win-hip-radeon-x64.zip",
"llama-b9000-bin-win-vulkan-x64.zip",
"llama-b9000-xcframework.zip",
];
const B6000: &[&str] = &[
"cudart-llama-bin-win-cuda-12.4-x64.zip",
"llama-b6000-bin-macos-x64.zip",
"llama-b6000-bin-ubuntu-vulkan-x64.zip",
"llama-b6000-bin-ubuntu-x64.zip",
"llama-b6000-bin-win-cpu-x64.zip",
"llama-b6000-bin-win-cuda-12.4-x64.zip",
"llama-b6000-bin-win-hip-radeon-x64.zip",
];
fn release(tag: &str, names: &[&str]) -> Release {
Release {
tag_name: tag.to_string(),
published_at: Some("2026-09-09T17:29:28Z".to_string()),
assets: names
.iter()
.enumerate()
.map(|(i, n)| Asset {
name: (*n).to_string(),
size: (i as u64 + 1) << 20,
digest: Some(format!("sha256:{}", "0".repeat(64))),
browser_download_url: format!("https://example.invalid/{n}"),
})
.collect(),
}
}
fn ids(release: &Release, os: &str, arch: &str) -> Vec<String> {
backends(release, os, arch)
.into_iter()
.map(|b| b.id)
.collect()
}
#[test]
fn platform_tokens_map_only_what_upstream_publishes() {
assert_eq!(os_token("windows"), Some("win"));
assert_eq!(os_token("linux"), Some("ubuntu"));
assert_eq!(os_token("macos"), Some("macos"));
assert_eq!(os_token("freebsd"), None);
assert_eq!(arch_token("x86_64"), Some("x64"));
assert_eq!(arch_token("aarch64"), Some("arm64"));
assert_eq!(arch_token("riscv64"), None);
}
#[test]
fn windows_x64_backends_of_the_newest_build() {
assert_eq!(
ids(&release("b10883", B10883), "windows", "x86_64"),
[
"cpu",
"cuda-12.4",
"cuda-13.3",
"openvino-2026.3.1",
"rocm-10.0",
"sycl",
"vulkan",
]
);
}
#[test]
fn linux_x64_reads_an_empty_middle_as_cpu() {
let got = ids(&release("b10883", B10883), "linux", "x86_64");
assert!(got.contains(&"cpu".to_string()), "{got:?}");
assert_eq!(
got,
[
"cpu",
"openvino-2026.3.1",
"rocm-10.0",
"sycl-fp16",
"sycl-fp32",
"vulkan",
]
);
}
#[test]
fn arm64_rows_are_kept_for_arm64_hosts_only() {
assert_eq!(
ids(&release("b10883", B10883), "windows", "aarch64"),
["cpu", "cuda-13.4", "opencl-adreno"]
);
assert_eq!(
ids(&release("b10883", B10883), "linux", "aarch64"),
["cpu", "vulkan"]
);
}
#[test]
fn non_server_and_foreign_assets_are_skipped() {
for name in [
"llama-b10883-ui.tar.gz",
"llama-b10883-xcframework.zip",
"llama-b10883-bin-android-arm64.tar.gz",
"llama-b10883-bin-ubuntu-s390x.tar.gz",
"cudart-llama-bin-win-cuda-12.4-x64.zip",
] {
assert_eq!(backend_of(name, "b10883", "ubuntu", "x64"), None, "{name}");
assert_eq!(backend_of(name, "b10883", "win", "x64"), None, "{name}");
}
for name in [
"llama-b9000-bin-310p-openEuler-x86.tar.gz",
"llama-b9000-bin-910b-openEuler-x86-aclgraph.tar.gz",
"llama-b9000-bin-macos-arm64-kleidiai.tar.gz",
] {
assert_eq!(backend_of(name, "b9000", "ubuntu", "x64"), None, "{name}");
assert_eq!(backend_of(name, "b9000", "macos", "arm64"), None, "{name}");
}
}
#[test]
fn a_renamed_backend_still_comes_through() {
assert_eq!(
ids(&release("b9000", B9000), "windows", "x86_64"),
["cpu", "cuda-12.4", "hip-radeon", "vulkan"]
);
}
#[test]
fn the_extension_comes_from_the_name_not_the_platform() {
assert_eq!(
backend_of("llama-b6000-bin-ubuntu-x64.zip", "b6000", "ubuntu", "x64"),
Some(("cpu".to_string(), Ext::Zip))
);
assert_eq!(
backend_of(
"llama-b10883-bin-ubuntu-x64.tar.gz",
"b10883",
"ubuntu",
"x64"
),
Some(("cpu".to_string(), Ext::TarGz))
);
assert_eq!(
ids(&release("b6000", B6000), "linux", "x86_64"),
["cpu", "vulkan"]
);
}
#[test]
fn cuda_backends_are_paired_with_their_runtime() {
let rel = release("b10883", B10883);
let found = backends(&rel, "windows", "x86_64");
let cuda = found.iter().find(|b| b.id == "cuda-12.4").unwrap();
assert_eq!(
cuda.cudart.as_ref().map(|a| a.name.as_str()),
Some("cudart-llama-bin-win-cuda-12.4-x64.zip")
);
assert!(cuda.needs_cudart() && !cuda.cudart_missing());
assert_eq!(
cuda.download_size(),
cuda.asset.size + cuda.cudart.as_ref().unwrap().size
);
let vulkan = found.iter().find(|b| b.id == "vulkan").unwrap();
assert!(!vulkan.needs_cudart() && vulkan.cudart.is_none());
}
#[test]
fn a_cuda_backend_without_a_runtime_is_flagged() {
let names: Vec<&str> = B10883
.iter()
.copied()
.filter(|n| *n != "cudart-llama-bin-win-cuda-12.4-x64.zip")
.collect();
let rel = release("b10883", &names);
let found = backends(&rel, "windows", "x86_64");
let cuda = found.iter().find(|b| b.id == "cuda-12.4").unwrap();
assert!(cuda.cudart_missing());
assert!(
!found
.iter()
.find(|b| b.id == "cuda-13.3")
.unwrap()
.cudart_missing()
);
}
#[test]
fn an_unsupported_platform_yields_nothing_rather_than_a_guess() {
assert!(backends(&release("b10883", B10883), "freebsd", "x86_64").is_empty());
assert!(backends(&release("b10883", B10883), "linux", "riscv64").is_empty());
let loc = locale(Lang::En);
assert!(check_platform("freebsd", "x86_64", loc).is_err());
assert!(check_platform("linux", "x86_64", loc).is_ok());
}
#[test]
fn install_names_round_trip_through_a_dashed_backend() {
assert_eq!(install_name("cuda-12.4", "b10883"), "cuda-12.4-b10883");
assert_eq!(
split_install_name("cuda-12.4-b10883"),
Some(("cuda-12.4", "b10883"))
);
assert_eq!(split_install_name("cpu-b10883"), Some(("cpu", "b10883")));
assert_eq!(split_install_name("nodash"), None);
}
#[test]
fn only_a_well_formed_sha256_digest_is_accepted() {
let hex = "8c79a9b226de4b3cacfd1f83d24f962d0773be79f1e7b75c6af4ded7e32ae1d6";
assert_eq!(digest_hex(&format!("sha256:{hex}")), Some(hex));
assert_eq!(digest_hex(&format!("sha512:{hex}")), None);
assert_eq!(digest_hex("sha256:abc"), None);
assert_eq!(digest_hex(&format!("sha256:{}", "z".repeat(64))), None);
}
#[test]
fn the_installed_build_is_compared_with_the_tag() {
assert_eq!(tag_build_number("b10883"), Some(10883));
assert_eq!(tag_build_number("v0.4.0"), None);
assert_eq!(
version_build_number("version: 0.3.0-dev (build 10807, commit 163a40796)"),
Some(10807)
);
assert_eq!(version_build_number("built with MSVC 19.51 for x64"), None);
}
#[test]
fn list_devices_is_read_as_a_list_or_as_none() {
assert!(parse_devices("Available devices:\n (none)\n").is_empty());
assert_eq!(
parse_devices("Available devices:\n CUDA0: RTX 4090 (24564 MiB)\n\n"),
["CUDA0: RTX 4090 (24564 MiB)"]
);
assert!(parse_devices("").is_empty());
}
#[test]
fn sha_mismatch_error_is_localized() {
let bad = [0u8; 32];
let en = verify_sha256(&bad, &"a".repeat(64), "asset.zip", locale(Lang::En))
.unwrap_err()
.to_string();
let ru = verify_sha256(&bad, &"a".repeat(64), "asset.zip", locale(Lang::Ru))
.unwrap_err()
.to_string();
assert!(
!en.chars().any(|c| ('\u{0400}'..='\u{04FF}').contains(&c)),
"{en}"
);
assert!(
ru.chars().any(|c| ('\u{0400}'..='\u{04FF}').contains(&c)),
"{ru}"
);
assert!(en.contains("asset.zip"));
}
#[test]
fn the_backends_listing_marks_what_is_already_installed() {
let dir = tempfile::tempdir().unwrap();
std::fs::create_dir_all(dir.path().join("vulkan-b10883")).unwrap();
let listing = Listing {
tag: "b10883".to_string(),
date: "2026-09-09".to_string(),
backends: backends(&release("b10883", B10883), std::env::consts::OS, "x86_64"),
};
let lines = render_backends(&listing, dir.path(), locale(Lang::En));
assert!(lines[0].contains("b10883") && lines[0].contains("2026-09-09"));
let vulkan = lines.iter().find(|l| l.contains("vulkan")).unwrap();
assert!(vulkan.contains("MB"), "{vulkan}");
assert!(
vulkan.contains(locale(Lang::En).t("llamacpp.backends.installed")),
"{vulkan}"
);
}
#[test]
fn installed_lists_finished_directories_and_skips_staging() {
let dir = tempfile::tempdir().unwrap();
let good = dir.path().join("cuda-12.4-b10883");
std::fs::create_dir_all(&good).unwrap();
std::fs::write(good.join(server_binary_name()), b"x").unwrap();
std::fs::create_dir_all(dir.path().join("cpu-b10871")).unwrap();
std::fs::create_dir_all(dir.path().join(".tmp-cpu-b10883")).unwrap();
let found = installed(dir.path());
assert_eq!(
found
.iter()
.map(|i| i.dir.file_name().unwrap().to_string_lossy().to_string())
.collect::<Vec<_>>(),
["cpu-b10871", "cuda-12.4-b10883"]
);
assert!(!found[0].binary_ok, "an empty directory has no binary");
assert!(found[1].binary_ok);
assert_eq!(found[1].backend, "cuda-12.4");
assert_eq!(found[1].tag, "b10883");
let lines = render_installed(&found, dir.path(), locale(Lang::En));
assert!(lines.iter().any(|l| l.contains("cuda-12.4-b10883")));
assert_eq!(
render_installed(&[], dir.path(), locale(Lang::En)),
[locale(Lang::En).t("llamacpp.installed.none")]
);
}
fn zip_with(entries: &[(&str, &[u8])]) -> Vec<u8> {
use std::io::Write;
let mut buf = Vec::new();
{
let mut w = zip::ZipWriter::new(std::io::Cursor::new(&mut buf));
for (name, body) in entries {
w.start_file::<_, ()>(*name, zip::write::SimpleFileOptions::default())
.unwrap();
w.write_all(body).unwrap();
}
w.finish().unwrap();
}
buf
}
fn targz_with(entries: &[(&str, &[u8])]) -> Vec<u8> {
let mut gz = flate2::write::GzEncoder::new(Vec::new(), flate2::Compression::fast());
{
let mut tar = tar::Builder::new(&mut gz);
for (name, body) in entries {
let mut header = tar::Header::new_gnu();
header.set_size(body.len() as u64);
header.set_mode(0o750);
header.set_cksum();
tar.append_data(&mut header, name, *body).unwrap();
}
tar.finish().unwrap();
}
gz.finish().unwrap()
}
#[test]
fn a_single_root_directory_is_collapsed_for_both_layouts() {
let loc = locale(Lang::En);
for (name, bytes) in [
(
"llama-b1-bin-win-cpu-x64.zip",
zip_with(&[("llama-server.exe", b"a"), ("llama.dll", b"b")]),
),
(
"llama-b1-bin-ubuntu-x64.tar.gz",
targz_with(&[
("llama-b1/llama-server", b"a"),
("llama-b1/libllama.so", b"b"),
]),
),
] {
let dir = tempfile::tempdir().unwrap();
let archive = dir.path().join(name);
std::fs::write(&archive, &bytes).unwrap();
let raw = dir.path().join("raw");
extract(&archive, name, &raw, loc).unwrap();
let out = dir.path().join("unpacked");
merge_payload(&raw, &out, loc).unwrap();
let mut got: Vec<String> = std::fs::read_dir(&out)
.unwrap()
.flatten()
.map(|e| e.file_name().to_string_lossy().to_string())
.collect();
got.sort();
assert_eq!(got.len(), 2, "{name}: {got:?}");
assert!(got.iter().any(|g| g.starts_with("llama-server")), "{got:?}");
}
}
#[test]
fn a_flat_archive_is_left_alone() {
let loc = locale(Lang::En);
let dir = tempfile::tempdir().unwrap();
let name = "cudart-llama-bin-win-cuda-12.4-x64.zip";
let archive = dir.path().join(name);
std::fs::write(
&archive,
zip_with(&[("cudart64_12.dll", b"a"), ("cublas64_12.dll", b"b")]),
)
.unwrap();
let raw = dir.path().join("raw");
extract(&archive, name, &raw, loc).unwrap();
let out = dir.path().join("unpacked");
std::fs::create_dir_all(&out).unwrap();
std::fs::write(out.join("llama-server.exe"), b"x").unwrap();
merge_payload(&raw, &out, loc).unwrap();
assert!(out.join("cudart64_12.dll").is_file());
assert!(
out.join("llama-server.exe").is_file(),
"the build stays put"
);
}
#[test]
fn an_unknown_archive_kind_is_refused() {
let loc = locale(Lang::En);
let dir = tempfile::tempdir().unwrap();
let archive = dir.path().join("build.7z");
std::fs::write(&archive, b"x").unwrap();
assert!(extract(&archive, "build.7z", &dir.path().join("raw"), loc).is_err());
}
#[test]
fn an_escaping_zip_entry_is_refused() {
let loc = locale(Lang::En);
let dir = tempfile::tempdir().unwrap();
let archive = dir.path().join("evil.zip");
std::fs::write(&archive, zip_with(&[("../escaped.txt", b"x")])).unwrap();
let err = extract(&archive, "evil.zip", &dir.path().join("raw"), loc).unwrap_err();
assert!(err.to_string().contains("escaped.txt"), "{err}");
assert!(!dir.path().join("escaped.txt").exists());
}
fn install_dir(root: &Path, name: &str, binary: bool) -> PathBuf {
let dir = root.join(name);
std::fs::create_dir_all(&dir).unwrap();
if binary {
std::fs::write(dir.join(server_binary_name()), b"x").unwrap();
}
dir
}
#[test]
fn an_explicit_path_is_never_second_guessed() {
let dir = tempfile::tempdir().unwrap();
install_dir(dir.path(), "cpu-b10883", true);
let typed = dir.path().join("nowhere").join("llama-server");
assert_eq!(
resolve_binary(Some(&typed.display().to_string()), None, Some(dir.path())),
Some(typed)
);
}
#[test]
fn a_bare_name_prefers_the_application_directory_then_path() {
let dir = tempfile::tempdir().unwrap();
let beside = dir.path().join("llama-server");
std::fs::write(&beside, b"x").unwrap();
assert_eq!(
resolve_binary(Some("llama-server"), Some(dir.path()), None),
Some(beside)
);
let empty = tempfile::tempdir().unwrap();
assert_eq!(
resolve_binary(Some("llama-server"), Some(empty.path()), None),
Some(PathBuf::from("llama-server")),
"nothing beside the app: hand the name to the OS, as before"
);
}
#[test]
fn an_empty_setting_finds_a_downloaded_build() {
let dir = tempfile::tempdir().unwrap();
let install = install_dir(dir.path(), "vulkan-b10883", true);
for configured in [None, Some(""), Some(" ")] {
assert_eq!(
resolve_binary(configured, None, Some(dir.path())),
Some(install.join(server_binary_name())),
"{configured:?}"
);
}
}
#[test]
fn the_build_installed_last_wins_over_the_newer_tag() {
let dir = tempfile::tempdir().unwrap();
install_dir(dir.path(), "cpu-b10883", true);
std::thread::sleep(std::time::Duration::from_millis(20));
let wanted = install_dir(dir.path(), "vulkan-b10871", true);
assert_eq!(
resolve_binary(None, None, Some(dir.path())),
Some(wanted.join(server_binary_name()))
);
}
#[test]
fn an_interrupted_install_and_a_staging_directory_are_not_candidates() {
let dir = tempfile::tempdir().unwrap();
let good = install_dir(dir.path(), "cpu-b10871", true);
std::thread::sleep(std::time::Duration::from_millis(20));
install_dir(dir.path(), "vulkan-b10883", false); install_dir(dir.path(), ".tmp-rocm-10.0-b10883", true); std::fs::write(dir.path().join("stray-file"), b"x").unwrap(); assert_eq!(
resolve_binary(None, None, Some(dir.path())),
Some(good.join(server_binary_name()))
);
}
#[test]
fn an_empty_setting_falls_back_to_the_application_directory() {
let app = tempfile::tempdir().unwrap();
let data = tempfile::tempdir().unwrap();
assert_eq!(
resolve_binary(None, Some(app.path()), Some(data.path())),
None,
"nothing anywhere is still nothing"
);
let beside = app.path().join(server_binary_name());
std::fs::write(&beside, b"x").unwrap();
assert_eq!(
resolve_binary(None, Some(app.path()), Some(data.path())),
Some(beside.clone())
);
let install = install_dir(data.path(), "cpu-b10883", true);
assert_eq!(
resolve_binary(None, Some(app.path()), Some(data.path())),
Some(install.join(server_binary_name()))
);
}
#[test]
fn without_directories_to_search_nothing_changes() {
assert_eq!(resolve_binary(None, None, None), None);
assert_eq!(
resolve_binary(Some("llama-server"), None, None),
Some(PathBuf::from("llama-server")),
);
}
#[test]
fn set_binary_writes_the_assistant_and_fills_only_empty_siblings() {
let mut config = AppConfig::default();
config.embed.managed.binary = Some("C:/mine/llama-server.exe".to_string());
let path = Path::new("C:/data/llama/vulkan-b10883/llama-server.exe");
let t = set_engine_binary(&mut config, path);
assert!(t.assistant && t.impersonation);
assert!(!t.embed, "a path the user typed is never overwritten");
assert_eq!(
config.engine.managed.binary.as_deref(),
Some("C:/data/llama/vulkan-b10883/llama-server.exe")
);
assert_eq!(
config.impersonation_engine.managed.binary.as_deref(),
Some("C:/data/llama/vulkan-b10883/llama-server.exe")
);
assert_eq!(
config.embed.managed.binary.as_deref(),
Some("C:/mine/llama-server.exe")
);
}
#[test]
fn a_blank_sibling_counts_as_empty() {
let mut config = AppConfig::default();
config.impersonation_engine.managed.binary = Some(" ".to_string());
let t = set_engine_binary(&mut config, Path::new("/data/llama/cpu-b1/llama-server"));
assert!(t.impersonation && t.embed);
}
#[test]
fn the_assistant_path_is_replaced_by_a_newer_install() {
let mut config = AppConfig::default();
config.engine.managed.binary = Some("/data/llama/cpu-b10871/llama-server".to_string());
set_engine_binary(
&mut config,
Path::new("/data/llama/cpu-b10883/llama-server"),
);
assert_eq!(
config.engine.managed.binary.as_deref(),
Some("/data/llama/cpu-b10883/llama-server")
);
}
#[test]
fn the_mode_is_reported_never_changed() {
let mut config = AppConfig::default();
assert!(set_engine_binary(&mut config, Path::new("/x/llama-server")).mode_is_managed);
config.engine.mode = ServerMode::Claude;
let t = set_engine_binary(&mut config, Path::new("/x/llama-server"));
assert!(!t.mode_is_managed);
assert_eq!(config.engine.mode, ServerMode::Claude);
let loc = locale(Lang::En);
let lines = render_binary_targets(&t, Path::new("/x/llama-server"), loc);
assert!(lines[0].contains("/x/llama-server"));
assert!(
lines
.iter()
.any(|l| l == loc.t("llamacpp.setbinary.not_managed")),
"{lines:?}"
);
}
#[test]
fn nothing_extra_is_reported_when_nothing_extra_was_written() {
let mut config = AppConfig::default();
config.impersonation_engine.managed.binary = Some("/a".to_string());
config.embed.managed.binary = Some("/b".to_string());
let t = set_engine_binary(&mut config, Path::new("/x/llama-server"));
let lines = render_binary_targets(&t, Path::new("/x/llama-server"), locale(Lang::En));
assert_eq!(lines.len(), 1, "{lines:?}");
}
#[test]
fn an_install_is_found_by_its_full_name() {
let dir = tempfile::tempdir().unwrap();
install_dir(dir.path(), "cuda-12.4-b10883", true);
let found = find_install(dir.path(), "cuda-12.4-b10883").unwrap();
assert_eq!(found.backend, "cuda-12.4");
assert_eq!(found.tag, "b10883");
assert!(find_install(dir.path(), "cuda-12.4-b10871").is_none());
assert!(find_install(dir.path(), "nope").is_none());
}
#[test]
fn a_bare_backend_resolves_only_while_it_is_unambiguous() {
let dir = tempfile::tempdir().unwrap();
install_dir(dir.path(), "vulkan-b10883", true);
assert_eq!(
find_install(dir.path(), "vulkan").map(|i| i.tag),
Some("b10883".to_string())
);
install_dir(dir.path(), "vulkan-b10871", true);
assert!(find_install(dir.path(), "vulkan").is_none());
assert!(find_install(dir.path(), "vulkan-b10871").is_some());
}
#[test]
fn the_settings_pointing_into_a_build_are_named() {
let dir = tempfile::tempdir().unwrap();
let install = install_dir(dir.path(), "cpu-b10883", true);
let other = install_dir(dir.path(), "vulkan-b10883", true);
let binary = install.join(server_binary_name());
let mut config = AppConfig::default();
assert!(binary_uses(&config, &install).is_empty(), "nothing set");
config.engine.managed.binary = Some(binary.display().to_string());
config.embed.managed.binary = Some(other.join(server_binary_name()).display().to_string());
assert_eq!(binary_uses(&config, &install), [BinaryUse::Assistant]);
assert_eq!(binary_uses(&config, &other), [BinaryUse::Embed]);
config.impersonation_engine.managed.binary = Some(binary.display().to_string());
assert_eq!(
binary_uses(&config, &install),
[BinaryUse::Assistant, BinaryUse::Impersonation]
);
}
#[test]
fn a_path_outside_the_build_is_not_a_use() {
let dir = tempfile::tempdir().unwrap();
let install = install_dir(dir.path(), "cpu-b10883", true);
for configured in [
None,
Some(""),
Some(" "),
Some("C:/elsewhere/llama-server"),
] {
assert!(!points_inside(configured, &install), "{configured:?}");
}
let sibling = dir.path().join("cpu-b10883-old").join("llama-server");
assert!(!points_inside(
Some(&sibling.display().to_string()),
&install
));
}
#[test]
fn removing_a_build_frees_it_and_leaves_the_others() {
let loc = locale(Lang::En);
let dir = tempfile::tempdir().unwrap();
let doomed = install_dir(dir.path(), "cpu-b10871", true);
std::thread::sleep(std::time::Duration::from_millis(20));
let kept = install_dir(dir.path(), "vulkan-b10883", true);
let install = find_install(dir.path(), "cpu-b10871").unwrap();
remove_install(&install.dir, loc).unwrap();
assert!(!doomed.exists() && kept.exists());
assert_eq!(installed(dir.path()).len(), 1);
let lines = render_removed(&install, dir.path(), None, loc);
assert!(lines[0].contains("cpu-b10871"), "{lines:?}");
assert!(
lines[1].contains("vulkan-b10883"),
"the empty field's new answer is reported: {lines:?}"
);
}
#[test]
fn removing_the_last_build_says_there_is_nothing_left() {
let loc = locale(Lang::En);
let dir = tempfile::tempdir().unwrap();
install_dir(dir.path(), "cpu-b10883", true);
let install = find_install(dir.path(), "cpu-b10883").unwrap();
remove_install(&install.dir, loc).unwrap();
let lines = render_removed(&install, dir.path(), None, loc);
assert_eq!(lines[1], loc.t("llamacpp.remove.nothing_left"));
}
#[test]
fn the_refusal_names_the_build_and_every_setting_that_points_at_it() {
let loc = locale(Lang::En);
let dir = tempfile::tempdir().unwrap();
install_dir(dir.path(), "cuda-12.4-b10883", true);
let install = find_install(dir.path(), "cuda-12.4-b10883").unwrap();
let msg = render_in_use(&[BinaryUse::Assistant, BinaryUse::Embed], &install, loc);
assert!(msg.contains("cuda-12.4-b10883"), "{msg}");
assert!(
msg.contains(loc.t("llamacpp.setbinary.assistant_name")),
"{msg}"
);
assert!(msg.contains(loc.t("llamacpp.setbinary.embed")), "{msg}");
assert!(msg.contains("--force"), "the way out is named: {msg}");
}
#[test]
#[ignore = "queries the GitHub releases API"]
fn live_the_newest_build_still_names_a_cpu_backend() {
let loc = locale(Lang::En);
let rt = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.unwrap();
let listing = rt.block_on(list_backends(None, loc)).unwrap();
let ids: Vec<&str> = listing.backends.iter().map(|b| b.id.as_str()).collect();
println!("build {} ({}): {ids:?}", listing.tag, listing.date);
assert!(tag_build_number(&listing.tag).is_some(), "{}", listing.tag);
assert!(ids.contains(&"cpu"), "{ids:?}");
for b in &listing.backends {
assert!(
b.asset.digest.as_deref().and_then(digest_hex).is_some(),
"{} has no usable digest",
b.asset.name
);
assert!(!b.cudart_missing(), "{} has no CUDA runtime", b.id);
}
}
#[test]
#[ignore = "downloads the ~18 MB cpu build from GitHub"]
fn live_install_cpu_into_a_tempdir() {
let loc = locale(Lang::En);
let dir = tempfile::tempdir().unwrap();
let rt = tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()
.unwrap();
let out = rt
.block_on(setup(
dir.path(),
&SetupOptions {
backend: "cpu".to_string(),
build: None,
force: false,
cudart: true,
},
loc,
|m| println!("{m}"),
))
.unwrap();
assert!(out.binary.is_file(), "{:?}", out.binary);
assert_eq!(
version_build_number(&out.version),
tag_build_number(&out.tag),
"{}",
out.version
);
let found = installed(dir.path());
assert_eq!(found.len(), 1);
assert!(found[0].binary_ok && found[0].bytes > 0);
assert!(
!dir.path()
.join(format!(".tmp-{}", install_name("cpu", &out.tag)))
.exists(),
"staging is removed on success"
);
}
}