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