pub mod generate;
pub mod verify;
pub use generate::generate_lock;
#[allow(unused_imports)]
pub use verify::{VerificationResult, VerificationStatus, verify_lock};
use std::collections::HashMap;
use std::fs;
use std::path::Path;
use serde::{Deserialize, Serialize};
pub const LOCK_VERSION: &str = "1";
#[allow(dead_code)] pub const LOCK_FILE_NAME: &str = "jarvy.lock";
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LockFile {
pub version: String,
pub meta: LockMeta,
pub tools: HashMap<String, LockedTool>,
#[serde(default)]
pub platforms: HashMap<String, HashMap<String, LockedTool>>,
}
impl LockFile {
pub fn new() -> Self {
Self {
version: LOCK_VERSION.to_string(),
meta: LockMeta::default(),
tools: HashMap::new(),
platforms: HashMap::new(),
}
}
pub fn load(path: &Path) -> Result<Self, LockError> {
let content = fs::read_to_string(path).map_err(|e| LockError::IoError {
path: path.display().to_string(),
error: e.to_string(),
})?;
toml::from_str(&content).map_err(|e| LockError::ParseError {
path: path.display().to_string(),
error: e.to_string(),
})
}
pub fn save(&self, path: &Path) -> Result<(), LockError> {
let content = toml::to_string_pretty(self).map_err(|e| LockError::SerializeError {
error: e.to_string(),
})?;
fs::write(path, content).map_err(|e| LockError::IoError {
path: path.display().to_string(),
error: e.to_string(),
})
}
pub fn get_tool(&self, name: &str, platform: &str) -> Option<&LockedTool> {
if let Some(platform_tools) = self.platforms.get(platform) {
if let Some(tool) = platform_tools.get(name) {
return Some(tool);
}
}
self.tools.get(name)
}
#[allow(dead_code)] pub fn set_tool(&mut self, name: &str, tool: LockedTool) {
self.tools.insert(name.to_string(), tool);
}
#[allow(dead_code)] pub fn set_platform_tool(&mut self, platform: &str, name: &str, tool: LockedTool) {
self.platforms
.entry(platform.to_string())
.or_default()
.insert(name.to_string(), tool);
}
}
impl Default for LockFile {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LockMeta {
pub generated: u64,
pub jarvy_version: String,
pub platform: String,
pub arch: String,
}
impl Default for LockMeta {
fn default() -> Self {
use std::time::{SystemTime, UNIX_EPOCH};
Self {
generated: SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0),
jarvy_version: env!("CARGO_PKG_VERSION").to_string(),
platform: std::env::consts::OS.to_string(),
arch: std::env::consts::ARCH.to_string(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LockedTool {
pub version: String,
pub source: InstallSource,
#[serde(default)]
pub checksum: Option<String>,
#[serde(default)]
pub binary_path: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "lowercase")]
pub enum InstallSource {
Brew,
BrewCask,
Apt,
Dnf,
Pacman,
Apk,
Pkg,
Winget,
Choco,
Custom(String),
Unknown,
}
impl std::fmt::Display for InstallSource {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
InstallSource::Brew => write!(f, "brew"),
InstallSource::BrewCask => write!(f, "brew-cask"),
InstallSource::Apt => write!(f, "apt"),
InstallSource::Dnf => write!(f, "dnf"),
InstallSource::Pacman => write!(f, "pacman"),
InstallSource::Apk => write!(f, "apk"),
InstallSource::Pkg => write!(f, "pkg"),
InstallSource::Winget => write!(f, "winget"),
InstallSource::Choco => write!(f, "choco"),
InstallSource::Custom(name) => write!(f, "custom:{}", name),
InstallSource::Unknown => write!(f, "unknown"),
}
}
}
#[derive(Debug)]
pub enum LockError {
IoError { path: String, error: String },
ParseError { path: String, error: String },
SerializeError { error: String },
#[allow(dead_code)] ToolNotFound { name: String },
#[allow(dead_code)] VersionMismatch {
tool: String,
locked: String,
installed: String,
},
#[allow(dead_code)] ChecksumMismatch {
tool: String,
locked: String,
computed: String,
},
}
impl std::fmt::Display for LockError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
LockError::IoError { path, error } => {
write!(f, "I/O error at '{}': {}", path, error)
}
LockError::ParseError { path, error } => {
write!(f, "Failed to parse lock file '{}': {}", path, error)
}
LockError::SerializeError { error } => {
write!(f, "Failed to serialize lock file: {}", error)
}
LockError::ToolNotFound { name } => {
write!(f, "Tool '{}' not found in lock file", name)
}
LockError::VersionMismatch {
tool,
locked,
installed,
} => {
write!(
f,
"Version mismatch for '{}': locked={}, installed={}",
tool, locked, installed
)
}
LockError::ChecksumMismatch {
tool,
locked,
computed,
} => {
write!(
f,
"Checksum mismatch for '{}': locked={}, computed={}",
tool, locked, computed
)
}
}
}
}
impl std::error::Error for LockError {}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
#[test]
fn test_lock_file_new() {
let lock = LockFile::new();
assert_eq!(lock.version, LOCK_VERSION);
assert!(lock.tools.is_empty());
}
#[test]
fn test_lock_file_save_load() {
let temp_dir = TempDir::new().unwrap();
let path = temp_dir.path().join("jarvy.lock");
let mut lock = LockFile::new();
lock.set_tool(
"git",
LockedTool {
version: "2.45.0".to_string(),
source: InstallSource::Brew,
checksum: None,
binary_path: Some("/opt/homebrew/bin/git".to_string()),
},
);
lock.save(&path).unwrap();
assert!(path.exists());
let loaded = LockFile::load(&path).unwrap();
assert_eq!(loaded.tools.len(), 1);
let git = loaded.tools.get("git").unwrap();
assert_eq!(git.version, "2.45.0");
assert_eq!(git.source, InstallSource::Brew);
}
#[test]
fn test_platform_specific_tools() {
let mut lock = LockFile::new();
lock.set_tool(
"git",
LockedTool {
version: "2.45.0".to_string(),
source: InstallSource::Brew,
checksum: None,
binary_path: None,
},
);
lock.set_platform_tool(
"linux",
"git",
LockedTool {
version: "2.40.0".to_string(),
source: InstallSource::Apt,
checksum: None,
binary_path: None,
},
);
let macos_git = lock.get_tool("git", "macos").unwrap();
assert_eq!(macos_git.version, "2.45.0");
let linux_git = lock.get_tool("git", "linux").unwrap();
assert_eq!(linux_git.version, "2.40.0");
}
#[test]
fn test_install_source_display() {
assert_eq!(InstallSource::Brew.to_string(), "brew");
assert_eq!(
InstallSource::Custom("nvm".to_string()).to_string(),
"custom:nvm"
);
}
#[test]
fn test_lock_meta_default() {
let meta = LockMeta::default();
assert!(!meta.jarvy_version.is_empty());
assert!(!meta.platform.is_empty());
assert!(!meta.arch.is_empty());
}
}