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 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#[derive(Debug, Clone, Deserialize, Serialize)]
137#[allow(dead_code)]
138pub struct State {
139 #[serde(skip)]
141 filename: PathBuf,
142
143 #[serde(skip)]
145 statedir: PathBuf,
146
147 latest_commit: Option<String>,
149}
150
151impl State {
152 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 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 pub fn statedir(&self) -> &Path {
190 &self.statedir
191 }
192
193 pub fn artifactsdir(&self) -> PathBuf {
195 self.statedir.join("artifacts")
196 }
197
198 pub fn cachedir(&self) -> PathBuf {
200 self.statedir.join("cache")
201 }
202
203 pub fn dependenciesdir(&self) -> PathBuf {
205 self.statedir.join("dependencies")
206 }
207
208 pub fn latest_commit(&self) -> Option<&str> {
210 self.latest_commit.as_deref()
211 }
212
213 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 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 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 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 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}