use crate::error::Result;
use crate::paths::Paths;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct InstalledPackage {
pub version: String,
#[serde(default)]
pub revision: u32,
pub installed_at: String,
#[serde(default = "default_installed_by")]
pub installed_by: String,
#[serde(default)]
pub requested: bool,
#[serde(default)]
pub pinned: bool,
#[serde(default)]
pub dependencies: Vec<String>,
#[serde(default)]
pub head_sha: Option<String>,
#[serde(default)]
pub is_head: bool,
#[serde(default)]
pub bottle_sha256: Option<String>,
}
fn default_installed_by() -> String {
"stout".to_string()
}
impl InstalledPackage {
pub fn is_head_install(&self) -> bool {
self.is_head || self.version.starts_with("HEAD")
}
pub fn short_sha(&self) -> Option<&str> {
if self.version.starts_with("HEAD-") {
Some(&self.version[5..])
} else {
None
}
}
}
#[derive(Debug, Default, Clone, Serialize, Deserialize)]
pub struct InstalledPackages {
#[serde(default)]
pub packages: HashMap<String, InstalledPackage>,
}
impl InstalledPackages {
pub fn load(paths: &Paths) -> Result<Self> {
let file_path = paths.installed_file();
if file_path.exists() {
let contents = std::fs::read_to_string(&file_path)?;
let packages: InstalledPackages = toml::from_str(&contents)?;
Ok(packages)
} else {
Ok(Self::default())
}
}
pub fn save(&self, paths: &Paths) -> Result<()> {
let file_path = paths.installed_file();
if let Some(parent) = file_path.parent() {
std::fs::create_dir_all(parent)?;
}
let contents = toml::to_string_pretty(self)?;
std::fs::write(&file_path, contents)?;
Ok(())
}
pub fn add(&mut self, name: &str, version: &str, revision: u32, requested: bool) {
self.add_with_deps(name, version, revision, requested, Vec::new());
}
pub fn add_with_deps(
&mut self,
name: &str,
version: &str,
revision: u32,
requested: bool,
dependencies: Vec<String>,
) {
let now = chrono_lite_now();
let pinned = self.packages.get(name).map(|p| p.pinned).unwrap_or(false);
self.packages.insert(
name.to_string(),
InstalledPackage {
version: version.to_string(),
revision,
installed_at: now,
installed_by: "stout".to_string(),
requested,
pinned,
dependencies,
head_sha: None,
is_head: version.starts_with("HEAD"),
bottle_sha256: None,
},
);
}
#[allow(clippy::too_many_arguments)]
pub fn add_imported(
&mut self,
name: &str,
version: &str,
revision: u32,
requested: bool,
installed_by: &str,
installed_at: &str,
dependencies: Vec<String>,
) {
let pinned = self.packages.get(name).map(|p| p.pinned).unwrap_or(false);
self.packages.insert(
name.to_string(),
InstalledPackage {
version: version.to_string(),
revision,
installed_at: installed_at.to_string(),
installed_by: installed_by.to_string(),
requested,
pinned,
dependencies,
head_sha: None,
is_head: version.starts_with("HEAD"),
bottle_sha256: None,
},
);
}
pub fn add_head(
&mut self,
name: &str,
short_sha: &str,
full_sha: &str,
requested: bool,
dependencies: Vec<String>,
) {
let now = chrono_lite_now();
let pinned = self.packages.get(name).map(|p| p.pinned).unwrap_or(false);
self.packages.insert(
name.to_string(),
InstalledPackage {
version: format!("HEAD-{}", short_sha),
revision: 0,
installed_at: now,
installed_by: "stout".to_string(),
requested,
pinned,
dependencies,
head_sha: Some(full_sha.to_string()),
is_head: true,
bottle_sha256: None,
},
);
}
pub fn pin(&mut self, name: &str) -> bool {
if let Some(pkg) = self.packages.get_mut(name) {
pkg.pinned = true;
true
} else {
false
}
}
pub fn unpin(&mut self, name: &str) -> bool {
if let Some(pkg) = self.packages.get_mut(name) {
pkg.pinned = false;
true
} else {
false
}
}
pub fn is_pinned(&self, name: &str) -> bool {
self.packages.get(name).map(|p| p.pinned).unwrap_or(false)
}
pub fn pinned(&self) -> impl Iterator<Item = (&String, &InstalledPackage)> {
self.packages.iter().filter(|(_, p)| p.pinned)
}
pub fn remove(&mut self, name: &str) -> Option<InstalledPackage> {
self.packages.remove(name)
}
pub fn get(&self, name: &str) -> Option<&InstalledPackage> {
self.packages.get(name)
}
pub fn is_installed(&self, name: &str) -> bool {
self.packages.contains_key(name)
}
pub fn is_version_installed(&self, name: &str, version: &str) -> bool {
self.packages
.get(name)
.map(|p| p.version == version)
.unwrap_or(false)
}
pub fn names(&self) -> impl Iterator<Item = &String> {
self.packages.keys()
}
pub fn count(&self) -> usize {
self.packages.len()
}
pub fn requested(&self) -> impl Iterator<Item = (&String, &InstalledPackage)> {
self.packages.iter().filter(|(_, p)| p.requested)
}
pub fn dependencies(&self) -> impl Iterator<Item = (&String, &InstalledPackage)> {
self.packages.iter().filter(|(_, p)| !p.requested)
}
pub fn iter(&self) -> impl Iterator<Item = (&String, &InstalledPackage)> {
self.packages.iter()
}
}
fn chrono_lite_now() -> String {
jiff::Timestamp::now()
.strftime("%Y-%m-%dT%H:%M:%SZ")
.to_string()
}