use regex::Regex;
use serde::{Deserialize, Serialize};
use std::fs;
use std::path::PathBuf;
const FILE_NAME: &str = "platformio.lock";
#[derive(Debug, PartialEq, Serialize, Deserialize)]
pub enum Category {
Platform,
Framework,
Tool,
Library,
}
#[derive(Debug, PartialEq, Serialize, Deserialize)]
pub struct Dependency {
pub name: String,
pub version: String,
pub category: Category,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct Lockfile {
pub version: u8,
pub dependencies: Vec<Dependency>,
}
impl Lockfile {
pub fn new(dependencies: Vec<Dependency>, version: u8) -> Lockfile {
Lockfile { version, dependencies }
}
pub fn load(proj_dir: &PathBuf) -> Result<Lockfile, String> {
let lock_path = proj_dir.join(FILE_NAME);
if !lock_path.exists() {
return Err("PlatformIO lock file does not exists.".to_string());
}
if let Ok(contents) = fs::read_to_string(lock_path) {
let data: Lockfile = match toml::from_str(&contents) {
Ok(d) => d,
Err(_) => {
return Err("Failed to parse PlatformIO lock file.".to_string());
}
};
Ok(data)
} else {
Err("Failed to read PlatformIO lock file.".to_string())
}
}
pub fn get_platform_packages(&self) -> Vec<String> {
self.dependencies
.iter()
.filter(|dep| dep.category == Category::Framework)
.map(|dep| format!("\n {} @ {}", dep.name, dep.version))
.collect()
}
pub fn get_lib_deps(&self) -> Vec<String> {
self.dependencies
.iter()
.filter(|dep| dep.category == Category::Library)
.map(|dep| format!("/n {} @ {}", dep.name, dep.version))
.collect()
}
pub fn save(&self, proj_dir: &PathBuf) -> Result<(), String> {
let lock_path = proj_dir.join(FILE_NAME);
let toml_string = toml::to_string_pretty(self)
.map_err(|e| format!("Failed to serialize lockfile: {}", e))?;
fs::write(lock_path, toml_string)
.map_err(|e| format!("Failed to write lockfile: {}", e))
}
}
pub fn get_pio_lock_path(proj_dir: &PathBuf) -> PathBuf {
proj_dir.join(FILE_NAME)
}
pub fn parse_pio_list_output(stdout: &str) -> Result<Vec<Dependency>, String> {
let package_regex = match Regex::new(r"([a-zA-Z0-9\-_/]+)\s+@\s+([a-zA-Z0-9\.\-\+]+)") {
Ok(regex) => regex,
Err(_) => {
return Err("Failed to parse regex expression".to_string());
}
};
let mut locked_deps = Vec::new();
let mut in_libraries_section = false;
for line in stdout.lines() {
if line.starts_with("Libraries") {
in_libraries_section = true;
continue;
}
if line.trim().is_empty() || !line.contains('@') {
continue;
}
if let Some(captures) = package_regex.captures(line) {
let name = match captures.get(1) {
Some(n) => n.as_str().to_string(),
None => {
return Err("Internal error while getting name.".to_string());
}
};
let version = match captures.get(2) {
Some(v) => v.as_str().to_string(),
None => {
return Err("Internal error while getting version.".to_string());
}
};
let category = if line.starts_with("Platform") {
Category::Platform
} else if name.starts_with("framework-") {
Category::Framework
} else if name.starts_with("tool-") || name.starts_with("toolchain-") {
Category::Tool
} else if in_libraries_section {
Category::Library
} else {
Category::Library
};
locked_deps.push(Dependency { name, version, category });
}
}
Ok(locked_deps)
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::tempdir;
#[test]
fn test_parse_pio_list_output() {
let sample_output = "
Platform atmelavr @ 5.0.0
Libraries
├── SomeLib @ 1.2.3
└── tool-avrdude @ 1.0.0
";
let deps = parse_pio_list_output(sample_output).unwrap();
assert_eq!(deps.len(), 3);
assert_eq!(deps[0].name, "atmelavr");
assert_eq!(deps[0].version, "5.0.0");
assert_eq!(deps[0].category, Category::Platform);
assert_eq!(deps[1].name, "SomeLib");
assert_eq!(deps[1].version, "1.2.3");
assert_eq!(deps[1].category, Category::Library);
assert_eq!(deps[2].name, "tool-avrdude");
assert_eq!(deps[2].category, Category::Tool);
}
#[test]
fn test_parse_pio_list_output_framework() {
let sample_output = "
├── framework-arduino-avr @ 1.8.3
";
let deps = parse_pio_list_output(sample_output).unwrap();
assert_eq!(deps.len(), 1);
assert_eq!(deps[0].name, "framework-arduino-avr");
assert_eq!(deps[0].version, "1.8.3");
assert_eq!(deps[0].category, Category::Framework);
}
#[test]
fn test_lockfile_new() {
let deps = vec![
Dependency {
name: "my_lib".to_string(),
version: "1.0".to_string(),
category: Category::Library,
}
];
let lockfile = Lockfile::new(deps, 1);
assert_eq!(lockfile.version, 1);
assert_eq!(lockfile.dependencies.len(), 1);
assert_eq!(lockfile.dependencies[0].name, "my_lib");
}
#[test]
fn test_get_platform_packages() {
let deps = vec![
Dependency { name: "framework-arduino".to_string(), version: "2.0".to_string(), category: Category::Framework },
Dependency { name: "toolchain-atmel".to_string(), version: "3.0".to_string(), category: Category::Tool },
];
let lockfile = Lockfile::new(deps, 1);
let frameworks = lockfile.get_platform_packages();
assert_eq!(frameworks.len(), 1);
assert_eq!(frameworks[0], "\n framework-arduino @ 2.0");
}
#[test]
fn test_get_lib_deps() {
let deps = vec![
Dependency { name: "SomeLib".to_string(), version: "1.2".to_string(), category: Category::Library },
Dependency { name: "atmelavr".to_string(), version: "4.0".to_string(), category: Category::Platform },
];
let lockfile = Lockfile::new(deps, 1);
let libs = lockfile.get_lib_deps();
assert_eq!(libs.len(), 1);
assert_eq!(libs[0], "/n SomeLib @ 1.2");
}
#[test]
fn test_save_and_load_lockfile() {
let temp_dir = tempdir().unwrap();
let proj_path = temp_dir.path().to_path_buf();
let deps = vec![
Dependency { name: "TestFramework".to_string(), version: "1.0.0".to_string(), category: Category::Framework },
Dependency { name: "TestLib".to_string(), version: "2.5.1".to_string(), category: Category::Library },
];
let original_lockfile = Lockfile::new(deps, 1);
let save_result = original_lockfile.save(&proj_path);
assert!(save_result.is_ok());
assert!(proj_path.join(FILE_NAME).exists());
let loaded_lockfile = Lockfile::load(&proj_path).expect("Failed to load lockfile");
assert_eq!(loaded_lockfile.version, 1);
assert_eq!(loaded_lockfile.dependencies.len(), 2);
assert_eq!(loaded_lockfile.dependencies[0].name, "TestFramework");
assert_eq!(loaded_lockfile.dependencies[1].name, "TestLib");
}
#[test]
fn test_load_nonexistent_lockfile() {
let temp_dir = tempdir().unwrap();
let proj_path = temp_dir.path().to_path_buf();
let result = Lockfile::load(&proj_path);
assert!(result.is_err());
assert_eq!(result.unwrap_err(), "PlatformIO lock file does not exists.");
}
#[test]
fn test_get_pio_lock_path() {
let proj_dir = PathBuf::from("/mock/project");
let lock_path = get_pio_lock_path(&proj_dir);
assert_eq!(lock_path, PathBuf::from("/mock/project/platformio.lock"));
}
}