use std::{
collections::HashMap,
fs::{File, OpenOptions},
io::ErrorKind,
path::PathBuf,
sync::{Mutex, MutexGuard, OnceLock},
};
use fs4::fs_std::FileExt;
use serde::{Deserialize, Serialize};
use crate::{
error::{LmcppError, LmcppResult},
server::{
toolchain::builder::{
ArgSet, ComputeBackend, ComputeBackendConfig, LMCPP_SERVER_EXECUTABLE,
LmcppBuildInstallMode, LmcppBuildInstallStatus, LmcppToolChain, LmcppToolchainOutcome,
},
types::file::{ValidDir, ValidFile},
},
};
static IN_PROC_LOCKS: OnceLock<Mutex<HashMap<PathBuf, &'static Mutex<()>>>> = OnceLock::new();
pub struct LmcppRecipe {
pub cfg: LmcppToolchainState,
pub mode: LmcppBuildInstallMode,
pub expected_build_args: ArgSet,
pub root_dir: ValidDir,
pub working_dir: ValidDir,
pub bin_dir: ValidDir,
pub fail_limit: u8,
pub version: String,
pub project: String,
}
impl LmcppRecipe {
const RECIPE_NAME: &'static str = "llama_cpp";
const LLAMA_CPP_REPO_URL: &str = "https://github.com/ggml-org/llama.cpp";
const LLAMA_CPP_ENV_OVERRIDE: &str = "LLAMA_CPP_INSTALL_DIR";
pub fn new(
project: &str,
override_root: &Option<ValidDir>,
fail_limit: u8,
repo_tag: &str,
compute_cfg: &ComputeBackendConfig,
mode: &LmcppBuildInstallMode,
build_args: &ArgSet,
) -> LmcppResult<Self> {
assert!(!project.is_empty(), "Project name cannot be empty");
assert!(!repo_tag.is_empty(), "Repo tag cannot be empty");
assert!(fail_limit > 0, "Fail limit must be greater than zero");
assert!(
!build_args.iter().any(|s| s.is_empty()),
"A build argument cannot be empty"
);
let mut build_args = build_args.clone();
let compute_backend: ComputeBackend = compute_cfg.to_backend(mode)?;
#[cfg(any(target_os = "linux", windows))]
{
if matches!(compute_backend, ComputeBackend::Cuda) {
build_args.insert(LmcppToolChain::CUDA_ARG.to_owned());
}
}
#[cfg(target_os = "macos")]
{
build_args.insert("-DBUILD_SHARED_LIBS=OFF".to_owned());
if matches!(compute_backend, ComputeBackend::Metal) {
build_args.insert(LmcppToolChain::METAL_ON.to_owned());
} else {
build_args.insert(LmcppToolChain::METAL_OFF.to_owned());
}
}
let version = format!("llama_cpp_{}_{}", repo_tag, compute_backend);
let root_dir = Self::resolve_root(override_root.as_ref(), &project)?;
let working_dir = ValidDir::new(root_dir.join(&version).join("working_dir"))?;
let bin_dir = ValidDir::new(root_dir.join(&version).join("bin"))?;
#[cfg(debug_assertions)]
{
debug_assert!(working_dir.starts_with(&root_dir), "must be in root_dir");
debug_assert!(bin_dir.starts_with(&root_dir), "must be in root_dir");
#[cfg(unix)] {
use std::os::unix::fs::MetadataExt;
let dev_root = std::fs::metadata(&root_dir).unwrap().dev();
debug_assert!(
dev_root == std::fs::metadata(&working_dir).unwrap().dev()
&& dev_root == std::fs::metadata(&bin_dir).unwrap().dev(),
"root_dir, working_dir and bin_dir must be on the same filesystem"
);
}
#[cfg(windows)]
{
let root_canon = root_dir.canonicalize().unwrap();
let working_canon = working_dir.canonicalize().unwrap();
let bin_canon = bin_dir.canonicalize().unwrap();
let root_drive = root_canon.components().next().unwrap();
let working_drive = working_canon.components().next().unwrap();
let bin_drive = bin_canon.components().next().unwrap();
debug_assert!(
root_drive == working_drive && root_drive == bin_drive,
"root_dir, working_dir and bin_dir must be on the same drive"
);
}
}
let mut cfg: LmcppToolchainState =
confy::load(&project, Some(version.as_str())).map_err(|e| {
LmcppError::file_system(
"loading confy configuration file",
"confy derives path",
std::io::Error::new(std::io::ErrorKind::Other, e.to_string()),
)
})?;
if let Some(ref cfg_repo_tag) = cfg.repo_tag {
debug_assert!(*cfg_repo_tag == repo_tag, "Repo tag mismatch");
} else {
cfg.repo_tag = Some(repo_tag.to_string());
}
if let Some(ref cfg_compute_backend) = cfg.compute_backend {
debug_assert!(
*cfg_compute_backend == compute_backend,
"Compute backend mismatch"
);
} else {
cfg.compute_backend = Some(compute_backend);
}
Ok(Self {
cfg,
mode: mode.clone(),
root_dir,
working_dir,
bin_dir,
fail_limit,
version,
project: project.to_owned(),
expected_build_args: build_args.clone(),
})
}
pub fn run(&mut self) -> LmcppResult<LmcppToolchainOutcome> {
let t0 = std::time::Instant::now();
let exec: std::result::Result<ValidFile, _> =
ValidFile::find_specific_file(&self.bin_dir, LMCPP_SERVER_EXECUTABLE);
if let Ok(ref bin_path) = exec {
if self.fingerprint_matches().is_ok() {
return Ok(LmcppToolchainOutcome {
duration: t0.elapsed(),
bin_path: Some(bin_path.clone()),
status: self.cfg.status.clone(),
repo_tag: self
.cfg
.repo_tag
.as_ref()
.expect("Repo tag set in constructor")
.clone(),
compute_backend: self
.cfg
.compute_backend
.as_ref()
.expect("Compute backend set in constructor")
.clone(),
executable_name: LMCPP_SERVER_EXECUTABLE.to_string(),
error: None,
});
}
}
let _lock = self.lock_file()?;
self.reset_toolchain()?;
let working_dir = self.working_dir.clone();
let uncheked_result = match self.mode {
LmcppBuildInstallMode::InstallOnly => self.install_prebuilt(&working_dir),
LmcppBuildInstallMode::BuildOnly => self.build_from_source(&working_dir),
LmcppBuildInstallMode::BuildOrInstall => {
let res = self.build_from_source(&working_dir);
match res {
Ok(src_binary) => Ok(src_binary),
Err(_) => {
self.expected_build_args.clear();
self.install_prebuilt(&working_dir)
}
}
}
};
let inner_result = match uncheked_result {
Ok(src_binary) => {
self.finalise(src_binary)
}
Err(e) => {
crate::error!("Failed to run recipe for {}: {e}", Self::RECIPE_NAME);
Err(e)
}
};
let duration = t0.elapsed();
crate::trace!(
"{} build/install completed in {:02}:{:02}:{:02}.{:03} for {}",
Self::RECIPE_NAME,
duration.as_secs() / 3600,
(duration.as_secs() % 3600) / 60,
duration.as_secs() % 60,
duration.subsec_millis(),
self.root_dir.display()
);
match inner_result {
Ok(bin_path) => {
self.cfg.fail_count = 0;
self.store_cfg()?;
Ok(LmcppToolchainOutcome {
duration: t0.elapsed(),
bin_path: Some(bin_path.clone()),
status: self.cfg.status.clone(),
repo_tag: self
.cfg
.repo_tag
.as_ref()
.expect("Repo tag set in constructor")
.clone(),
compute_backend: self
.cfg
.compute_backend
.as_ref()
.expect("Compute backend set in constructor")
.clone(),
executable_name: LMCPP_SERVER_EXECUTABLE.to_string(),
error: None,
})
}
Err(e) => {
self.cfg.status = LmcppBuildInstallStatus::NotBuiltOrInstalled;
self.cfg.fail_count = self.cfg.fail_count.saturating_add(1);
let n = self.cfg.fail_count;
if n >= self.fail_limit {
crate::error!(
"{n} consecutive failures - purging {} and starting fresh",
self.root_dir.display()
);
self.reset_toolchain()?;
} else {
crate::error!(" {n} consecutive failures");
self.store_cfg()?;
};
Ok(LmcppToolchainOutcome {
duration: t0.elapsed(),
bin_path: None,
status: self.cfg.status.clone(),
repo_tag: self
.cfg
.repo_tag
.as_ref()
.expect("Repo tag set in constructor")
.clone(),
compute_backend: self
.cfg
.compute_backend
.as_ref()
.expect("Compute backend set in constructor")
.clone(),
executable_name: "error".to_string(),
error: Some(e),
})
}
}
}
pub fn validate(&mut self) -> LmcppResult<LmcppToolchainOutcome> {
let t0 = std::time::Instant::now();
let bin_path = ValidFile::find_specific_file(&self.bin_dir, LMCPP_SERVER_EXECUTABLE)?;
self.fingerprint_matches()?;
Ok(LmcppToolchainOutcome {
duration: t0.elapsed(),
bin_path: Some(bin_path),
status: self.cfg.status.clone(),
repo_tag: self
.cfg
.repo_tag
.as_ref()
.expect("Repo tag set in constructor")
.clone(),
compute_backend: self
.cfg
.compute_backend
.as_ref()
.expect("Compute backend set in constructor")
.clone(),
executable_name: LMCPP_SERVER_EXECUTABLE.to_string(),
error: None,
})
}
pub fn remove(&mut self) -> LmcppResult<()> {
{
let _lock = self.lock_file()?;
}
if self.root_dir.exists() {
self.root_dir.remove()?; }
Ok(())
}
fn install_prebuilt(&mut self, working_dir: &ValidDir) -> LmcppResult<ValidFile> {
let repo_tag = &self
.cfg
.repo_tag
.as_ref()
.expect("Repo tag set in constructor");
let repo_url = Self::LLAMA_CPP_REPO_URL;
let url = if cfg!(target_os = "linux") {
format!("{repo_url}/releases/download/{repo_tag}/llama-{repo_tag}-bin-ubuntu-x64.zip")
} else if cfg!(target_os = "macos") {
match std::env::consts::ARCH {
"aarch64" => format!(
"{repo_url}/releases/download/{repo_tag}/llama-{repo_tag}-bin-macos-arm64.zip"
),
"x86_64" => format!(
"{repo_url}/releases/download/{repo_tag}/llama-{repo_tag}-bin-macos-x64.zip"
),
arch => panic!("Unsupported architecture on macOS: {}", arch),
}
} else if cfg!(target_os = "windows") {
format!(
"{repo_url}/releases/download/{repo_tag}/llama-{repo_tag}-bin-win-cuda-12.4-x64.zip"
)
} else {
return Err(LmcppError::BackendUnavailable {
what: "Llama.cpp",
os: std::env::consts::OS,
arch: std::env::consts::ARCH,
reason: format!("Unsupported OS: {}", std::env::consts::OS),
});
};
super::zip::download_and_extract_zip(&url, working_dir, "llama_cpp_binary")?;
let bin_path = ValidFile::find_specific_file(working_dir, LMCPP_SERVER_EXECUTABLE)?;
self.cfg.status = LmcppBuildInstallStatus::Installed;
self.cfg.actual_build_args = ArgSet::default();
Ok(bin_path)
}
fn build_from_source(&mut self, working_dir: &ValidDir) -> LmcppResult<ValidFile> {
super::cmake::cmake_is_available()?;
let curl_disabled = self
.expected_build_args
.iter()
.any(|arg| arg == LmcppToolChain::CURL_OFF);
if !curl_disabled {
super::cmake::curl_is_available()?;
}
let repo_url = Self::LLAMA_CPP_REPO_URL;
let url = format!(
"{repo_url}/archive/refs/tags/{}.zip",
self.cfg
.repo_tag
.as_ref()
.expect("Repo tag set in constructor")
);
let build_args: Vec<&str> = self
.expected_build_args
.iter()
.map(String::as_str)
.collect();
super::zip::download_and_extract_zip(&url, working_dir, "llama_cpp_repo")?;
super::cmake::cmake_project_buildsystem(working_dir, &build_args)?;
super::cmake::cmake_build_project(working_dir)?;
let bin_path = ValidFile::find_specific_file(working_dir, LMCPP_SERVER_EXECUTABLE)?;
self.cfg.status = LmcppBuildInstallStatus::Built;
self.cfg.actual_build_args = self.expected_build_args.clone();
println!("actual build args: {:?}", self.cfg.actual_build_args);
println!("expected build args: {:?}", self.expected_build_args);
Ok(bin_path)
}
fn resolve_root(override_root: Option<&ValidDir>, project: &str) -> LmcppResult<ValidDir> {
let env_path = std::env::var_os(Self::LLAMA_CPP_ENV_OVERRIDE).map(PathBuf::from);
if let Some(p) = override_root {
return ValidDir::new(p);
}
if let Some(p) = env_path.as_ref() {
return ValidDir::new(p);
}
let project_dir = directories::ProjectDirs::from("com", project, Self::RECIPE_NAME)
.ok_or_else(|| {
LmcppError::file_system(
"resolve_root",
"directories derives path",
std::io::Error::new(
ErrorKind::NotFound,
"Failed to resolve platform-native data directory",
),
)
})?;
let p = ValidDir::new(project_dir.data_dir())?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
std::fs::set_permissions(&p, std::fs::Permissions::from_mode(0o755))
.map_err(|e| LmcppError::file_system("resolve_root", p.as_ref(), e))?;
}
Ok(p)
}
fn fingerprint_matches(&self) -> LmcppResult<()> {
match self.mode {
LmcppBuildInstallMode::InstallOnly => {
if self.cfg.status != LmcppBuildInstallStatus::Installed {
return Err(LmcppError::Fingerprint {
reason: format!("expected installed, found {:?}", self.cfg.status),
});
}
}
LmcppBuildInstallMode::BuildOnly => {
if self.cfg.status != LmcppBuildInstallStatus::Built {
return Err(LmcppError::Fingerprint {
reason: format!("expected built, found {:?}", self.cfg.status),
});
}
}
LmcppBuildInstallMode::BuildOrInstall => {
if self.cfg.status == LmcppBuildInstallStatus::NotBuiltOrInstalled {
return Err(LmcppError::Fingerprint {
reason: format!("expected built or installed, found {:?}", self.cfg.status),
});
}
}
}
if self.mode != LmcppBuildInstallMode::InstallOnly {
let args_match = self.expected_build_args.len() == self.cfg.actual_build_args.len()
&& self
.expected_build_args
.iter()
.all(|arg| self.cfg.actual_build_args.contains(arg));
if !args_match {
return Err(LmcppError::Fingerprint {
reason: format!(
"expected build arguments {:?}, found {:?}",
self.expected_build_args, self.cfg.actual_build_args
),
});
}
}
Ok(())
}
fn store_cfg(&self) -> LmcppResult<()> {
confy::store(&self.project, Some(self.version.as_str()), &self.cfg).map_err(|e| {
{
LmcppError::file_system(
"saving confy configuration file",
"confy derives path",
std::io::Error::new(std::io::ErrorKind::Other, e.to_string()),
)
}
})
}
fn reset_toolchain(&mut self) -> LmcppResult<()> {
if self.working_dir.exists() {
self.working_dir.remove()?;
}
if self.bin_dir.exists() {
self.bin_dir.remove()?;
}
self.cfg.fail_count = 0;
self.cfg.status = LmcppBuildInstallStatus::NotBuiltOrInstalled;
self.store_cfg()?;
Ok(())
}
fn finalise(&self, src_binary: ValidFile) -> LmcppResult<ValidFile> {
self.fingerprint_matches()?;
src_binary.make_executable()?;
let src_dir = src_binary.parent().ok_or_else(|| LmcppError::FileSystem {
operation: "get src_dir from src_binary",
path: src_binary.to_path_buf(),
source: std::io::Error::new(
ErrorKind::Other,
"Failed to determine parent directory of the binary".to_string(),
),
})?;
if self.bin_dir.exists() {
self.bin_dir.remove()?;
}
std::fs::rename(src_dir, &self.bin_dir).map_err(|e| {
LmcppError::file_system("move src_dir to bin_dir", self.bin_dir.as_ref(), e)
})?;
self.working_dir.reset()?;
ValidFile::find_specific_file(&self.bin_dir, LMCPP_SERVER_EXECUTABLE)
}
fn lock_file(&self) -> LmcppResult<(File, MutexGuard<'static, ()>)> {
let lock_path = self.root_dir.join(format!("{}.lock", self.version));
let guard = {
let mut map = IN_PROC_LOCKS
.get_or_init(|| Mutex::new(HashMap::new()))
.lock()
.unwrap();
let m: &'static Mutex<()> = *map
.entry(lock_path.clone())
.or_insert_with(|| Box::leak(Box::new(Mutex::new(()))));
m.try_lock().map_err(|e| {
LmcppError::file_system(
"tool-chain dir is already being modified by another thread",
self.root_dir.as_ref(),
std::io::Error::new(ErrorKind::Other, e.to_string()),
)
})?
};
let file = OpenOptions::new()
.read(true)
.write(true)
.create(true)
.open(&lock_path)
.map_err(|e| LmcppError::file_system("open lock file", &lock_path, e))?;
file.try_lock_exclusive().map_err(|e| match e.kind() {
ErrorKind::WouldBlock => LmcppError::file_system(
"tool-chain dir is already being modified by another *process*",
lock_path,
e,
),
_ => LmcppError::file_system("acquire directory lock", lock_path, e),
})?;
Ok((file, guard))
}
}
#[derive(Serialize, Deserialize, Default, Debug)]
pub struct LmcppToolchainState {
repo_tag: Option<String>,
status: LmcppBuildInstallStatus,
compute_backend: Option<ComputeBackend>,
actual_build_args: ArgSet,
fail_count: u8,
}
#[cfg(test)]
mod tests {
use tempfile::tempdir;
use super::*;
use crate::error::LmcppError;
struct DummyRecipe {
chain: LmcppRecipe,
}
impl DummyRecipe {
fn new(root: &std::path::Path, fail_limit: u8) -> Self {
let override_root = Some(ValidDir::new(root).unwrap());
let chain = LmcppRecipe::new(
"dummy_project", &override_root, fail_limit, "v0", &ComputeBackendConfig::Cpu, &LmcppBuildInstallMode::BuildOnly, &ArgSet::default(), )
.unwrap();
Self { chain }
}
fn run_toolchain(&mut self) -> LmcppResult<LmcppToolchainOutcome> {
let build_err = LmcppError::InvalidConfig {
field: "dummy_field",
reason: "Simulated failure for testing purposes".into(),
};
self.chain.cfg.fail_count = self.chain.cfg.fail_count.saturating_add(1);
let n = self.chain.cfg.fail_count;
if n >= self.chain.fail_limit {
self.chain.reset_toolchain()?; } else {
self.chain.store_cfg()?; }
Err(build_err.into())
}
}
#[test]
fn resets_after_n_fails() {
let tmp = tempdir().unwrap();
let root = tmp.path();
let mut recipe = DummyRecipe::new(root, 1);
let _ = recipe.run_toolchain().expect_err("must fail #1");
std::fs::create_dir_all(&recipe.chain.bin_dir).unwrap();
let sentry = recipe.chain.bin_dir.join("sentry");
std::fs::write(&sentry, b"x").unwrap();
assert!(sentry.exists(), "sentry file must exist before 2nd run");
let _ = recipe.run_toolchain().expect_err("must fail #2");
assert!(
!sentry.exists(),
"sentry file must be removed by reset_toolchain()"
);
}
#[test]
fn finalise_happy_path_moves_and_validates() -> LmcppResult<()> {
let tmp = tempdir().unwrap();
let override_root = Some(ValidDir::new(tmp.path())?);
let mut recipe = LmcppRecipe::new(
"finalise_test",
&override_root,
3, "v0",
&ComputeBackendConfig::Cpu,
&LmcppBuildInstallMode::BuildOnly, &ArgSet::default(),
)?;
recipe.cfg.status = LmcppBuildInstallStatus::Built;
std::fs::create_dir_all(&recipe.bin_dir).unwrap();
std::fs::write(recipe.bin_dir.join("stale"), b"x").unwrap();
let src_dir = recipe.working_dir.join("build_out");
std::fs::create_dir_all(&src_dir).unwrap();
let src_bin_path = src_dir.join(LMCPP_SERVER_EXECUTABLE);
std::fs::write(&src_bin_path, b"dummy-binary").unwrap();
let src_bin: ValidFile = src_bin_path.try_into()?;
recipe.cfg.actual_build_args = recipe.expected_build_args.clone();
let result = recipe.finalise(src_bin)?;
assert!(result.exists());
assert_eq!(
result.file_name().unwrap(),
std::ffi::OsStr::new(LMCPP_SERVER_EXECUTABLE)
);
let names: Vec<_> = std::fs::read_dir(&recipe.bin_dir)
.unwrap()
.map(|e| e.unwrap().file_name())
.collect();
assert_eq!(
names,
vec![std::ffi::OsString::from(LMCPP_SERVER_EXECUTABLE)]
);
assert!(
std::fs::read_dir(&recipe.working_dir)
.unwrap()
.next()
.is_none()
);
Ok(())
}
fn recipe_skeleton(mode: LmcppBuildInstallMode) -> LmcppRecipe {
let tmp_dir = tempfile::tempdir().unwrap();
let override_root = Some(ValidDir::new(&tmp_dir).unwrap());
LmcppRecipe::new(
"test_project", &override_root, 3, "test-tag", &ComputeBackendConfig::Cpu, &mode,
&ArgSet::default(), )
.expect("recipe construction must succeed")
}
#[test]
fn detects_status_mismatch() {
let mut recipe = recipe_skeleton(LmcppBuildInstallMode::BuildOnly);
recipe.cfg.status = LmcppBuildInstallStatus::Installed;
assert!(
recipe.fingerprint_matches().is_err(),
"status mismatch should be reported as an error"
);
}
#[test]
fn install_then_build_invalidates_fingerprint() {
let tmp_dir = tempfile::tempdir().unwrap();
let root_dir = ValidDir::new(&tmp_dir).unwrap();
let mut install_recipe = LmcppRecipe::new(
"test_project",
&Some(root_dir.clone()),
3,
"test-tag",
&ComputeBackendConfig::Cpu,
&LmcppBuildInstallMode::InstallOnly,
&ArgSet::default(),
)
.unwrap();
install_recipe.cfg.status = LmcppBuildInstallStatus::Installed;
install_recipe.store_cfg().unwrap();
let build_recipe = LmcppRecipe::new(
"test_project".into(),
&Some(root_dir),
3,
"test-tag".into(),
&ComputeBackendConfig::Cpu,
&LmcppBuildInstallMode::BuildOnly,
&ArgSet::default(),
)
.unwrap();
assert!(
build_recipe.fingerprint_matches().is_err(),
"install-only cache must be invalid when switching to build-only mode"
);
}
#[test]
fn in_process_lock_blocks_second_thread() {
let tmp_dir = tempfile::tempdir().unwrap();
let root_pb = tmp_dir.path().to_path_buf();
let root_arc = std::sync::Arc::new(root_pb);
let new_recipe = {
let root_arc = root_arc.clone();
move || {
let root_dir = ValidDir::new(&*root_arc).unwrap(); LmcppRecipe::new(
"lock_test",
&Some(root_dir),
3,
"v1",
&ComputeBackendConfig::Cpu,
&LmcppBuildInstallMode::BuildOrInstall,
&ArgSet::default(),
)
.unwrap()
}
};
let barrier = std::sync::Arc::new(std::sync::Barrier::new(2));
let barrier_a = barrier.clone();
let handle_a = std::thread::spawn({
let new_recipe = new_recipe.clone(); move || {
let recipe = new_recipe();
let _guard = recipe.lock_file().unwrap(); barrier_a.wait(); std::thread::sleep(std::time::Duration::from_millis(200));
}
});
let barrier_b = barrier.clone();
let handle_b = std::thread::spawn(move || {
barrier_b.wait(); let recipe = new_recipe();
let err = recipe
.lock_file()
.expect_err("second thread should be blocked by in‑process mutex");
assert!(
err.to_string()
.contains("already being modified by another thread"),
"unexpected error: {err}"
);
});
handle_a.join().unwrap();
handle_b.join().unwrap();
}
#[test]
fn resolve_root_precedence_and_permissions() -> LmcppResult<()> {
let override_dir = tempfile::tempdir().unwrap();
let env_dir = tempfile::tempdir().unwrap();
let env_key = LmcppRecipe::LLAMA_CPP_ENV_OVERRIDE;
unsafe {
std::env::set_var(env_key, env_dir.path());
}
let override_valid = ValidDir::new(override_dir.path())?;
let dir = LmcppRecipe::resolve_root(Some(&override_valid), "root_test")?;
let expected = std::fs::canonicalize(override_dir.path()).unwrap();
assert_eq!(dir.as_ref(), expected);
let dir = LmcppRecipe::resolve_root(None, "root_test")?;
let expected = std::fs::canonicalize(env_dir.path()).unwrap();
assert_eq!(dir.as_ref(), expected);
unsafe {
std::env::remove_var(env_key);
}
let dir = LmcppRecipe::resolve_root(None, "root_test")?;
let proj_dirs = directories::ProjectDirs::from("com", "root_test", "llama_cpp").unwrap();
assert_eq!(
dir.as_ref(),
std::fs::canonicalize(proj_dirs.data_dir()).unwrap()
);
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let mode = std::fs::metadata(&dir).unwrap().permissions().mode() & 0o777;
assert_eq!(mode, 0o755, "platform data dir should be chmod-ed to 755");
}
Ok(())
}
}