ambient_ci/
project.rs

1use std::{
2    collections::HashMap,
3    fs::{read, write},
4    io::Write,
5    path::{Path, PathBuf},
6};
7
8use clingwrap::tildepathbuf::TildePathBuf;
9use log::debug;
10use serde::{Deserialize, Serialize};
11
12use crate::{
13    action::{TrustedAction, UnsafeAction},
14    util::mkdir,
15};
16
17#[derive(Debug, Deserialize, Clone)]
18#[serde(deny_unknown_fields)]
19pub struct Projects {
20    projects: HashMap<String, Project>,
21}
22
23impl Projects {
24    pub fn from_file(filename: &Path) -> Result<Self, ProjectError> {
25        let dirname = if let Some(parent) = filename.parent() {
26            parent.to_path_buf()
27        } else {
28            return Err(ProjectError::Parent(filename.into()));
29        };
30
31        let yaml = read(filename).map_err(|e| ProjectError::Read(filename.into(), e))?;
32        let mut projects: Self =
33            serde_norway::from_slice(&yaml).map_err(|e| ProjectError::Yaml(filename.into(), e))?;
34
35        for (_, p) in projects.projects.iter_mut() {
36            p.expand_tilde(&dirname)?;
37        }
38
39        // debug!("projects from file {}: {:#?}", filename.display(), projects);
40        Ok(projects)
41    }
42
43    pub fn get(&self, name: &str) -> Option<&Project> {
44        self.projects.get(name)
45    }
46
47    pub fn iter(&self) -> impl Iterator<Item = (&str, &Project)> {
48        self.projects.iter().map(|(k, v)| (k.as_str(), v))
49    }
50}
51
52#[derive(Debug, Deserialize, Clone)]
53#[serde(deny_unknown_fields)]
54pub struct Project {
55    source: TildePathBuf,
56    #[serde(skip)]
57    expanded_source: PathBuf,
58
59    image: TildePathBuf,
60    #[serde(skip)]
61    expanded_image: PathBuf,
62
63    pre_plan: Option<Vec<TrustedAction>>,
64    plan: Option<Vec<UnsafeAction>>,
65    post_plan: Option<Vec<TrustedAction>>,
66    artifact_max_size: Option<u64>,
67    cache_max_size: Option<u64>,
68}
69
70impl Project {
71    fn expand_tilde(&mut self, basedir: &Path) -> Result<(), ProjectError> {
72        self.expanded_source = Self::abspath(basedir.join(self.source.path()))?;
73        self.expanded_image = Self::abspath(basedir.join(self.image.path()))?;
74        Ok(())
75    }
76
77    pub fn from_file(filename: &Path) -> Result<Self, ProjectError> {
78        let dirname = if let Some(parent) = filename.parent() {
79            parent.to_path_buf()
80        } else {
81            return Err(ProjectError::Parent(filename.into()));
82        };
83
84        let yaml = read(filename).map_err(|e| ProjectError::Read(filename.into(), e))?;
85        let mut project: Project =
86            serde_norway::from_slice(&yaml).map_err(|e| ProjectError::Yaml(filename.into(), e))?;
87
88        project.expand_tilde(&dirname)?;
89
90        debug!("project from file {}: {:#?}", filename.display(), project);
91        Ok(project)
92    }
93
94    fn abspath(path: PathBuf) -> Result<PathBuf, ProjectError> {
95        path.canonicalize()
96            .map_err(|e| ProjectError::Canonicalize(path, e))
97    }
98
99    pub fn source(&self) -> &Path {
100        &self.expanded_source
101    }
102
103    pub fn image(&self) -> &Path {
104        &self.expanded_image
105    }
106
107    pub fn artifact_max_size(&self) -> Option<u64> {
108        self.artifact_max_size
109    }
110
111    pub fn cache_max_size(&self) -> Option<u64> {
112        self.cache_max_size
113    }
114
115    pub fn pre_plan(&self) -> &[TrustedAction] {
116        if let Some(plan) = &self.pre_plan {
117            plan.as_slice()
118        } else {
119            &[]
120        }
121    }
122
123    pub fn plan(&self) -> &[UnsafeAction] {
124        if let Some(plan) = &self.plan {
125            plan.as_slice()
126        } else {
127            &[]
128        }
129    }
130
131    pub fn post_plan(&self) -> &[TrustedAction] {
132        if let Some(plan) = &self.post_plan {
133            plan.as_slice()
134        } else {
135            &[]
136        }
137    }
138}
139
140/// Persistent project state.
141#[derive(Debug, Clone, Deserialize, Serialize)]
142#[allow(dead_code)]
143pub struct State {
144    // File where this state is stored, if it's stored.
145    #[serde(skip)]
146    filename: PathBuf,
147
148    // Where persistent state is stored for this project.
149    #[serde(skip)]
150    statedir: PathBuf,
151
152    /// Latest commit that CI has run on, if any.
153    latest_commit: Option<String>,
154}
155
156impl State {
157    /// Load state for a project from a file, if it's present. If it's
158    /// not present, return an empty state.
159    pub fn from_file(statedir: &Path, project: &str) -> Result<Self, ProjectError> {
160        let statedir = statedir.join(project);
161        let filename = statedir.join("meta.yaml");
162        debug!("load project state from {}", filename.display());
163        let state = if filename.exists() {
164            let yaml = read(&filename).map_err(|e| ProjectError::ReadState(filename.clone(), e))?;
165            let mut state: Self = serde_norway::from_slice(&yaml)
166                .map_err(|e| ProjectError::ParseState(filename.clone(), e))?;
167            state.filename = filename;
168            state.statedir = statedir;
169            state
170        } else {
171            Self {
172                filename,
173                statedir,
174                latest_commit: None,
175            }
176        };
177
178        mkdir(&state.artifactsdir())?;
179        mkdir(&state.cachedir())?;
180        mkdir(&state.dependenciesdir())?;
181
182        Ok(state)
183    }
184
185    /// Write project state.
186    pub fn write_to_file(&self) -> Result<(), ProjectError> {
187        debug!("write project state to {}", self.filename.display());
188        let yaml = serde_norway::to_string(&self)
189            .map_err(|e| ProjectError::SerializeState(self.clone(), e))?;
190        if !self.statedir.exists() {
191            std::fs::create_dir(&self.statedir)
192                .map_err(|e| ProjectError::CreateState(self.statedir.clone(), e))?;
193        }
194        write(&self.filename, yaml)
195            .map_err(|e| ProjectError::WriteState(self.filename.clone(), e))?;
196        Ok(())
197    }
198
199    /// Return state directory.
200    pub fn statedir(&self) -> &Path {
201        &self.statedir
202    }
203
204    /// Return artifacts directory for project.
205    pub fn artifactsdir(&self) -> PathBuf {
206        self.statedir.join("artifacts")
207    }
208
209    /// Return cache directory for project.
210    pub fn cachedir(&self) -> PathBuf {
211        self.statedir.join("cache")
212    }
213
214    /// Return dependencies directory for a project.
215    pub fn dependenciesdir(&self) -> PathBuf {
216        self.statedir.join("dependencies")
217    }
218
219    /// Return latest commit that CI has run on.
220    pub fn latest_commit(&self) -> Option<&str> {
221        self.latest_commit.as_deref()
222    }
223
224    /// Set latest commit.
225    pub fn set_latest_commot(&mut self, commit: Option<&str>) {
226        self.latest_commit = commit.map(|s| s.into());
227    }
228
229    fn run_log_filename(&self) -> PathBuf {
230        self.statedir.join("run.log")
231    }
232
233    /// Remove any existing run log.
234    pub fn remove_run_log(&self) -> Result<(), ProjectError> {
235        let filename = self.run_log_filename();
236        debug!("removing run log file {}", filename.display());
237        debug!(
238            "statedir is {}, exists? {}",
239            self.statedir.display(),
240            self.statedir.exists()
241        );
242        if filename.exists() {
243            std::fs::remove_file(&filename)
244                .map_err(|err| ProjectError::RemoveRunLog(filename, err))?;
245        }
246        Ok(())
247    }
248
249    /// Create empty run log file. Return its filename.
250    pub fn create_run_log(&self) -> Result<PathBuf, ProjectError> {
251        let filename = self.run_log_filename();
252        debug!("creating run log file {}", filename.display());
253        std::fs::OpenOptions::new()
254            .create(true)
255            .write(true)
256            .truncate(true)
257            .open(&filename)
258            .map_err(|err| ProjectError::CreateRunLog(filename.clone(), err))?;
259        debug!("created run log file {} OK", filename.display());
260        Ok(filename)
261    }
262
263    /// Append data to run log. The file must already exist.
264    pub fn append_to_run_log(&self, data: &[u8]) -> Result<(), ProjectError> {
265        let filename = self.run_log_filename();
266        let mut file = std::fs::OpenOptions::new()
267            .append(true)
268            .open(&filename)
269            .map_err(|err| ProjectError::CreateRunLog(filename.clone(), err))?;
270
271        file.write_all(data)
272            .map_err(|err| ProjectError::AppendToRunLog(filename, err))?;
273
274        Ok(())
275    }
276
277    /// Return contents of run log.
278    pub fn read_run_log(&self) -> Result<Vec<u8>, ProjectError> {
279        let filename = self.run_log_filename();
280        let data =
281            std::fs::read(&filename).map_err(|err| ProjectError::ReadRunLog(filename, err))?;
282        Ok(data)
283    }
284}
285
286#[derive(Debug, thiserror::Error)]
287pub enum ProjectError {
288    #[error("failed to determine directory containing project file {0}")]
289    Parent(PathBuf),
290
291    #[error("failed to make filename absolute: {0}")]
292    Canonicalize(PathBuf, #[source] std::io::Error),
293
294    #[error("failed top read project file {0}")]
295    Read(PathBuf, #[source] std::io::Error),
296
297    #[error("failed to parse project file as YAML: {0}")]
298    Yaml(PathBuf, #[source] serde_norway::Error),
299
300    #[error("failed to serialize project state as YAML: {0:#?}")]
301    SerializeState(State, #[source] serde_norway::Error),
302
303    #[error("failed to write project state to file {0}")]
304    WriteState(PathBuf, #[source] std::io::Error),
305
306    #[error("failed to read project state from file {0}")]
307    ReadState(PathBuf, #[source] std::io::Error),
308
309    #[error("failed to parse project state file as YAML: {0}")]
310    ParseState(PathBuf, #[source] serde_norway::Error),
311
312    #[error("failed to create project state directory {0}")]
313    CreateState(PathBuf, #[source] std::io::Error),
314
315    #[error("failed to remove run log file {0}")]
316    RemoveRunLog(PathBuf, #[source] std::io::Error),
317
318    #[error("failed to create run log file {0}")]
319    CreateRunLog(PathBuf, #[source] std::io::Error),
320
321    #[error("failed to append to run log file {0}")]
322    AppendToRunLog(PathBuf, #[source] std::io::Error),
323
324    #[error("failed to read run log file {0}")]
325    ReadRunLog(PathBuf, #[source] std::io::Error),
326
327    #[error(transparent)]
328    MKdir(#[from] crate::util::UtilError),
329}