ambient_ci/
project.rs

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