Skip to main content

bv_core/
project.rs

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    /// SemVer version requirement, e.g. `">=0.7,<1"`. Empty means "latest".
14    #[serde(default)]
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/// `[runtime]` block in `bv.toml`. Selects the container backend.
44#[derive(Debug, Clone, Serialize, Deserialize, Default)]
45pub struct RuntimeConfig {
46    /// `"docker"`, `"apptainer"`, or `"auto"` (default).
47    #[serde(default, skip_serializing_if = "Option::is_none")]
48    pub backend: Option<String>,
49}
50
51/// Contents of `bv.toml`.
52#[derive(Debug, Clone, Serialize, Deserialize)]
53pub struct BvToml {
54    pub project: ProjectMeta,
55    #[serde(skip_serializing_if = "Option::is_none")]
56    pub registry: Option<RegistryConfig>,
57    #[serde(default)]
58    pub tools: Vec<ToolDeclaration>,
59    #[serde(default)]
60    pub data: HashMap<String, DataDeclaration>,
61    #[serde(default)]
62    pub hardware: HardwareProfile,
63    #[serde(default, skip_serializing_if = "runtime_config_is_default")]
64    pub runtime: RuntimeConfig,
65    /// Resolves collisions when two tools expose the same binary name.
66    /// Maps binary name to the tool id that should own the shim.
67    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
68    pub binary_overrides: HashMap<String, String>,
69}
70
71fn runtime_config_is_default(rc: &RuntimeConfig) -> bool {
72    rc.backend.is_none()
73}
74
75impl BvToml {
76    pub fn from_path(path: &Path) -> Result<Self> {
77        let s = fs::read_to_string(path)?;
78        toml::from_str(&s).map_err(|e| BvError::ManifestParse(e.to_string()))
79    }
80
81    pub fn to_path(&self, path: &Path) -> Result<()> {
82        let s = toml::to_string_pretty(self).map_err(|e| BvError::ManifestParse(e.to_string()))?;
83        atomic_write(path, &s)
84    }
85}
86
87pub struct BvLock;
88
89impl BvLock {
90    pub fn from_path(path: &Path) -> Result<Lockfile> {
91        let s = fs::read_to_string(path)?;
92        Lockfile::from_toml_str(&s)
93    }
94
95    pub fn to_path(lock: &Lockfile, path: &Path) -> Result<()> {
96        let s = lock.to_toml_string()?;
97        atomic_write(path, &s)
98    }
99}
100
101fn atomic_write(path: &Path, content: &str) -> Result<()> {
102    let parent = path.parent().unwrap_or(Path::new("."));
103    let tmp = tmp_path(parent);
104    fs::write(&tmp, content)?;
105    fs::rename(&tmp, path)?;
106    Ok(())
107}
108
109fn tmp_path(dir: &Path) -> PathBuf {
110    let id = std::time::SystemTime::now()
111        .duration_since(std::time::UNIX_EPOCH)
112        .map(|d| d.subsec_nanos())
113        .unwrap_or(0);
114    dir.join(format!(".bv-tmp-{id}"))
115}