use crate::id::Id;
use proto_pdk_api::{Checksum, ToolLockOptions};
use serde::{Deserialize, Serialize};
use starbase_utils::fs;
use starbase_utils::toml::{self, TomlError};
use std::collections::BTreeMap;
use std::fmt::Debug;
use std::path::{Path, PathBuf};
use system_env::{SystemArch, SystemOS};
use tracing::{debug, instrument};
use version_spec::{UnresolvedVersionSpec, VersionSpec};
pub const PROTO_LOCK_NAME: &str = ".protolock";
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
#[serde(default)]
pub struct LockRecord {
#[serde(skip_serializing_if = "Option::is_none")]
pub os: Option<SystemOS>,
#[serde(skip_serializing_if = "Option::is_none")]
pub arch: Option<SystemArch>,
#[serde(skip_serializing_if = "Option::is_none")]
pub backend: Option<Id>,
#[serde(skip_serializing_if = "Option::is_none")]
pub spec: Option<UnresolvedVersionSpec>,
#[serde(skip_serializing_if = "Option::is_none")]
pub version: Option<VersionSpec>,
#[serde(skip_serializing_if = "Option::is_none")]
pub checksum: Option<Checksum>,
#[serde(skip_serializing_if = "Option::is_none")]
pub source: Option<String>,
}
impl LockRecord {
pub fn for_manifest(&self) -> Self {
let mut record = self.clone();
record.spec = None;
record.version = None;
record
}
pub fn for_lockfile(&self) -> Self {
let mut record = self.clone();
record.source = None;
record
}
pub fn is_match(&self, other: &Self, options: &ToolLockOptions) -> bool {
self.is_match_with(
other.backend.as_ref(),
other.spec.as_ref(),
other.os.as_ref(),
other.arch.as_ref(),
options,
)
}
pub fn is_match_with(
&self,
backend: Option<&Id>,
spec: Option<&UnresolvedVersionSpec>,
os: Option<&SystemOS>,
arch: Option<&SystemArch>,
options: &ToolLockOptions,
) -> bool {
if self.backend.as_ref() != backend || self.spec.as_ref() != spec {
return false;
}
if options.ignore_os_arch {
if self.os.is_some() || self.arch.is_some() {
return false;
}
} else {
if self.os.is_some() && self.os.as_ref() != os
|| self.arch.is_some() && self.arch.as_ref() != arch
{
return false;
}
}
true
}
}
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
#[serde(default, deny_unknown_fields)]
pub struct ProtoLock {
#[serde(skip_serializing_if = "BTreeMap::is_empty")]
pub tools: BTreeMap<Id, Vec<LockRecord>>,
#[serde(skip)]
pub path: PathBuf,
}
impl ProtoLock {
pub fn load_from<P: AsRef<Path>>(dir: P) -> Result<Self, TomlError> {
Self::load(Self::resolve_path(dir))
}
#[instrument(name = "load_lock")]
pub fn load<P: AsRef<Path> + Debug>(path: P) -> Result<Self, TomlError> {
let path = path.as_ref();
debug!(file = ?path, "Loading lock file");
let mut manifest: ProtoLock = if path.exists() {
toml::read_file(path)?
} else {
ProtoLock::default()
};
manifest.path = path.into();
Ok(manifest)
}
#[instrument(name = "save_lock", skip(self))]
pub fn save(&self) -> Result<(), TomlError> {
if self.tools.is_empty() {
debug!(file = ?self.path, "Removing lock file because its empty");
fs::remove_file(&self.path)?;
return Ok(());
}
debug!(file = ?self.path, "Saving lock file");
let content = toml::format(self, true)?;
fs::write_file(
&self.path,
format!("# Generated by proto. Do not modify!\n\n{content}"),
)?;
Ok(())
}
pub fn sort_records(&mut self) {
for records in self.tools.values_mut() {
records.sort_by_key(|record| {
(
record.spec.clone(),
record.backend.clone(),
record.os,
record.arch,
)
});
}
}
fn resolve_path(path: impl AsRef<Path>) -> PathBuf {
let path = path.as_ref();
if path.ends_with(PROTO_LOCK_NAME) {
path.to_path_buf()
} else {
path.join(PROTO_LOCK_NAME)
}
}
}