1use std::collections::BTreeMap;
2use std::fs;
3use std::path::{Path, PathBuf};
4use std::sync::atomic::{AtomicU64, Ordering};
5
6use serde::{Deserialize, Serialize};
7
8use crate::error::{BvError, Result};
9use crate::lockfile::Lockfile;
10
11#[derive(Debug, Clone, Serialize, Deserialize)]
12pub struct ToolDeclaration {
13 pub id: String,
14 #[serde(default, skip_serializing_if = "String::is_empty")]
16 pub version: String,
17}
18
19#[derive(Debug, Clone, Serialize, Deserialize)]
20pub struct DataDeclaration {
21 pub id: String,
22 #[serde(default)]
23 pub version: String,
24}
25
26#[derive(Debug, Clone, Serialize, Deserialize, Default)]
27pub struct HardwareProfile {
28 pub gpu: Option<bool>,
29 pub cpu_cores: Option<u32>,
30 pub ram_gb: Option<f64>,
31}
32
33#[derive(Debug, Clone, Serialize, Deserialize)]
34pub struct ProjectMeta {
35 pub name: String,
36 pub description: Option<String>,
37}
38
39#[derive(Debug, Clone, Serialize, Deserialize)]
40pub struct RegistryConfig {
41 pub url: String,
42}
43
44#[derive(Debug, Clone, Serialize, Deserialize, Default)]
46pub struct RuntimeConfig {
47 #[serde(default, skip_serializing_if = "Option::is_none")]
49 pub backend: Option<String>,
50}
51
52#[derive(Debug, Clone, Serialize, Deserialize)]
65pub struct CacheMount {
66 #[serde(rename = "match", default = "default_match")]
68 pub tool_match: String,
69 pub container_path: String,
70 pub host_path: String,
71}
72
73fn default_match() -> String {
74 "*".to_string()
75}
76
77#[derive(Debug, Clone, Serialize, Deserialize)]
79pub struct BvToml {
80 pub project: ProjectMeta,
81 #[serde(skip_serializing_if = "Option::is_none")]
82 pub registry: Option<RegistryConfig>,
83 #[serde(default)]
84 pub tools: Vec<ToolDeclaration>,
85 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
86 pub data: BTreeMap<String, DataDeclaration>,
87 #[serde(default, skip_serializing_if = "hardware_profile_is_default")]
88 pub hardware: HardwareProfile,
89 #[serde(default, skip_serializing_if = "runtime_config_is_default")]
90 pub runtime: RuntimeConfig,
91 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
94 pub binary_overrides: BTreeMap<String, String>,
95 #[serde(default, rename = "cache", skip_serializing_if = "Vec::is_empty")]
100 pub caches: Vec<CacheMount>,
101}
102
103fn runtime_config_is_default(rc: &RuntimeConfig) -> bool {
104 rc.backend.is_none()
105}
106
107fn hardware_profile_is_default(h: &HardwareProfile) -> bool {
108 h.gpu.is_none() && h.cpu_cores.is_none() && h.ram_gb.is_none()
109}
110
111impl BvToml {
112 pub fn from_path(path: &Path) -> Result<Self> {
113 let s = fs::read_to_string(path)?;
114 toml::from_str(&s).map_err(|e| BvError::ManifestParse(e.to_string()))
115 }
116
117 pub fn to_path(&self, path: &Path) -> Result<()> {
118 let s = toml::to_string_pretty(self).map_err(|e| BvError::ManifestParse(e.to_string()))?;
119 atomic_write(path, &s)
120 }
121}
122
123pub struct BvLock;
124
125impl BvLock {
126 pub fn from_path(path: &Path) -> Result<Lockfile> {
127 let s = fs::read_to_string(path)?;
128 Lockfile::from_toml_str(&s)
129 }
130
131 pub fn to_path(lock: &Lockfile, path: &Path) -> Result<()> {
132 let s = lock.to_toml_string()?;
133 atomic_write(path, &s)
134 }
135}
136
137fn atomic_write(path: &Path, content: &str) -> Result<()> {
138 let parent = path.parent().unwrap_or(Path::new("."));
139 let tmp = tmp_path(parent);
140 fs::write(&tmp, content).map_err(|e| {
141 let _ = fs::remove_file(&tmp);
143 e
144 })?;
145 fs::rename(&tmp, path).map_err(|e| {
146 let _ = fs::remove_file(&tmp);
147 e
148 })?;
149 Ok(())
150}
151
152fn tmp_path(dir: &Path) -> PathBuf {
157 static COUNTER: AtomicU64 = AtomicU64::new(0);
158 let pid = std::process::id();
159 let nanos = std::time::SystemTime::now()
160 .duration_since(std::time::UNIX_EPOCH)
161 .map(|d| d.subsec_nanos() as u64)
162 .unwrap_or(0);
163 let seq = COUNTER.fetch_add(1, Ordering::Relaxed);
164 dir.join(format!(".bv-tmp-{pid}-{nanos:09}-{seq}"))
165}