1use std::{
4 collections::HashMap,
5 fs::{read, write},
6 io::Write,
7 path::{Path, PathBuf},
8};
9
10use clingwrap::tildepathbuf::TildePathBuf;
11use serde::{Deserialize, Serialize};
12
13use crate::{
14 action::{PostPlanAction, PrePlanAction, UnsafeAction},
15 util::mkdir,
16};
17
18#[derive(Debug, Deserialize, Clone)]
20#[serde(deny_unknown_fields)]
21pub struct Projects {
22 projects: HashMap<String, Project>,
23}
24
25impl Projects {
26 pub fn from_file(filename: &Path) -> Result<Self, ProjectError> {
28 let dirname = if let Some(parent) = filename.parent() {
29 parent.to_path_buf()
30 } else {
31 return Err(ProjectError::Parent(filename.into()));
32 };
33
34 let yaml = read(filename).map_err(|e| ProjectError::Read(filename.into(), e))?;
35 let mut projects: Self =
36 serde_norway::from_slice(&yaml).map_err(|e| ProjectError::Yaml(filename.into(), e))?;
37
38 for (name, p) in projects.projects.iter_mut() {
39 p.expand_tilde(&dirname)?;
40 if !p.expanded_source.is_dir() {
41 return Err(ProjectError::NotADirectory(
42 name.into(),
43 p.expanded_source.clone(),
44 ));
45 }
46 }
47
48 Ok(projects)
49 }
50
51 pub fn get(&self, name: &str) -> Option<&Project> {
53 self.projects.get(name)
54 }
55
56 pub fn iter(&self) -> impl Iterator<Item = (&str, &Project)> {
58 self.projects.iter().map(|(k, v)| (k.as_str(), v))
59 }
60}
61
62#[derive(Debug, Deserialize, Clone)]
64#[serde(deny_unknown_fields)]
65pub struct Project {
66 pub source: TildePathBuf,
68
69 #[serde(skip)]
70 expanded_source: PathBuf,
71
72 pub image: TildePathBuf,
74
75 #[serde(skip)]
76 expanded_image: PathBuf,
77
78 pub pre_plan: Option<Vec<PrePlanAction>>,
80
81 pub plan: Option<Vec<UnsafeAction>>,
83
84 pub post_plan: Option<Vec<PostPlanAction>>,
86
87 pub artifact_max_size: Option<u64>,
89
90 pub cache_max_size: Option<u64>,
92}
93
94impl Project {
95 fn expand_tilde(&mut self, basedir: &Path) -> Result<(), ProjectError> {
96 self.expanded_source = Self::abspath(basedir.join(self.source.path()))?;
97 self.expanded_image = Self::abspath(basedir.join(self.image.path()))?;
98 Ok(())
99 }
100
101 pub fn from_file(filename: &Path) -> Result<Self, ProjectError> {
103 let dirname = if let Some(parent) = filename.parent() {
104 parent.to_path_buf()
105 } else {
106 return Err(ProjectError::Parent(filename.into()));
107 };
108
109 let yaml = read(filename).map_err(|e| ProjectError::Read(filename.into(), e))?;
110 let mut project: Project =
111 serde_norway::from_slice(&yaml).map_err(|e| ProjectError::Yaml(filename.into(), e))?;
112
113 project.expand_tilde(&dirname)?;
114 if !project.expanded_source.is_dir() {
115 return Err(ProjectError::NotADirectory(
116 filename.to_string_lossy().to_string(),
117 project.expanded_source,
118 ));
119 }
120
121 Ok(project)
122 }
123
124 fn abspath(path: PathBuf) -> Result<PathBuf, ProjectError> {
125 path.canonicalize()
126 .map_err(|e| ProjectError::Canonicalize(path, e))
127 }
128
129 pub fn source(&self) -> &Path {
131 &self.expanded_source
132 }
133
134 pub fn image(&self) -> &Path {
136 &self.expanded_image
137 }
138
139 pub fn artifact_max_size(&self) -> Option<u64> {
141 self.artifact_max_size
142 }
143
144 pub fn cache_max_size(&self) -> Option<u64> {
146 self.cache_max_size
147 }
148
149 pub fn pre_plan(&self) -> &[PrePlanAction] {
151 if let Some(plan) = &self.pre_plan {
152 plan.as_slice()
153 } else {
154 &[]
155 }
156 }
157
158 pub fn plan(&self) -> &[UnsafeAction] {
160 if let Some(plan) = &self.plan {
161 plan.as_slice()
162 } else {
163 &[]
164 }
165 }
166
167 pub fn post_plan(&self) -> &[PostPlanAction] {
169 if let Some(plan) = &self.post_plan {
170 plan.as_slice()
171 } else {
172 &[]
173 }
174 }
175}
176
177#[derive(Debug, Clone, Deserialize, Serialize)]
179#[allow(dead_code)]
180pub struct State {
181 #[serde(skip)]
183 filename: PathBuf,
184
185 #[serde(skip)]
187 statedir: PathBuf,
188
189 pub latest_commit: Option<String>,
191}
192
193impl State {
194 pub fn from_file(statedir: &Path, project: &str) -> Result<Self, ProjectError> {
197 let statedir = statedir.join(project);
198 let filename = statedir.join("meta.yaml");
199 let state = if filename.exists() {
200 let yaml = read(&filename).map_err(|e| ProjectError::ReadState(filename.clone(), e))?;
201 let mut state: Self = serde_norway::from_slice(&yaml)
202 .map_err(|e| ProjectError::ParseState(filename.clone(), e))?;
203 state.filename = filename;
204 state.statedir = statedir;
205 state
206 } else {
207 Self {
208 filename,
209 statedir,
210 latest_commit: None,
211 }
212 };
213
214 mkdir(&state.artifactsdir())?;
215 mkdir(&state.cachedir())?;
216 mkdir(&state.dependenciesdir())?;
217
218 Ok(state)
219 }
220
221 pub fn write_to_file(&self) -> Result<(), ProjectError> {
223 let yaml = serde_norway::to_string(&self)
224 .map_err(|e| ProjectError::SerializeState(self.clone(), e))?;
225 if !self.statedir.exists() {
226 std::fs::create_dir(&self.statedir)
227 .map_err(|e| ProjectError::CreateState(self.statedir.clone(), e))?;
228 }
229 write(&self.filename, yaml)
230 .map_err(|e| ProjectError::WriteState(self.filename.clone(), e))?;
231 Ok(())
232 }
233
234 pub fn statedir(&self) -> &Path {
236 &self.statedir
237 }
238
239 pub fn artifactsdir(&self) -> PathBuf {
241 self.statedir.join("artifacts")
242 }
243
244 pub fn cachedir(&self) -> PathBuf {
246 self.statedir.join("cache")
247 }
248
249 pub fn dependenciesdir(&self) -> PathBuf {
251 self.statedir.join("dependencies")
252 }
253
254 pub fn latest_commit(&self) -> Option<&str> {
256 self.latest_commit.as_deref()
257 }
258
259 pub fn set_latest_commot(&mut self, commit: Option<&str>) {
261 self.latest_commit = commit.map(|s| s.into());
262 }
263
264 pub fn console_log_filename(&self) -> PathBuf {
266 self.statedir.join("console.log")
267 }
268
269 pub fn remove_console_log(&self) -> Result<(), ProjectError> {
271 let filename = self.console_log_filename();
272 if filename.exists() {
273 std::fs::remove_file(&filename)
274 .map_err(|err| ProjectError::RemoveConsoleLog(filename, err))?;
275 }
276 Ok(())
277 }
278
279 pub fn create_console_log(&self) -> Result<PathBuf, ProjectError> {
281 let filename = self.console_log_filename();
282 std::fs::OpenOptions::new()
283 .create(true)
284 .write(true)
285 .truncate(true)
286 .open(&filename)
287 .map_err(|err| ProjectError::CreateConsoleLog(filename.clone(), err))?;
288 Ok(filename)
289 }
290
291 pub fn append_to_console_log(&self, data: &[u8]) -> Result<(), ProjectError> {
293 let filename = self.console_log_filename();
294 let mut file = std::fs::OpenOptions::new()
295 .append(true)
296 .open(&filename)
297 .map_err(|err| ProjectError::CreateConsoleLog(filename.clone(), err))?;
298
299 file.write_all(data)
300 .map_err(|err| ProjectError::AppendToConsoleLog(filename, err))?;
301
302 Ok(())
303 }
304
305 pub fn read_console_log(&self) -> Result<Vec<u8>, ProjectError> {
307 let filename = self.run_log_filename();
308 let data =
309 std::fs::read(&filename).map_err(|err| ProjectError::ReadConsoleLog(filename, err))?;
310 Ok(data)
311 }
312
313 pub fn run_log_filename(&self) -> PathBuf {
315 self.statedir.join("run.log")
316 }
317
318 pub fn remove_run_log(&self) -> Result<(), ProjectError> {
320 let filename = self.run_log_filename();
321 if filename.exists() {
322 std::fs::remove_file(&filename)
323 .map_err(|err| ProjectError::RemoveRunLog(filename, err))?;
324 }
325 Ok(())
326 }
327
328 pub fn create_run_log(&self) -> Result<PathBuf, ProjectError> {
330 let filename = self.run_log_filename();
331 std::fs::OpenOptions::new()
332 .create(true)
333 .write(true)
334 .truncate(true)
335 .open(&filename)
336 .map_err(|err| ProjectError::CreateRunLog(filename.clone(), err))?;
337 Ok(filename)
338 }
339
340 pub fn create_raw_log(&self) -> Result<PathBuf, ProjectError> {
342 let filename = self.raw_log_filename();
343 std::fs::OpenOptions::new()
344 .create(true)
345 .write(true)
346 .truncate(true)
347 .open(&filename)
348 .map_err(|err| ProjectError::CreateRunLog(filename.clone(), err))?;
349 Ok(filename)
350 }
351
352 pub fn remove_raw_log(&self) -> Result<(), ProjectError> {
354 let filename = self.raw_log_filename();
355 if filename.exists() {
356 std::fs::remove_file(&filename)
357 .map_err(|err| ProjectError::RemoveRawLog(filename, err))?;
358 }
359 Ok(())
360 }
361
362 pub fn raw_log_filename(&self) -> PathBuf {
364 self.statedir.join("raw.log")
365 }
366
367 pub fn append_to_run_log(&self, data: &[u8]) -> Result<(), ProjectError> {
369 let filename = self.run_log_filename();
370 let mut file = std::fs::OpenOptions::new()
371 .append(true)
372 .open(&filename)
373 .map_err(|err| ProjectError::CreateRunLog(filename.clone(), err))?;
374
375 file.write_all(data)
376 .map_err(|err| ProjectError::AppendToRunLog(filename, err))?;
377
378 Ok(())
379 }
380
381 pub fn read_run_log(&self) -> Result<Vec<u8>, ProjectError> {
383 let filename = self.run_log_filename();
384 let data =
385 std::fs::read(&filename).map_err(|err| ProjectError::ReadRunLog(filename, err))?;
386 Ok(data)
387 }
388}
389
390#[derive(Debug, thiserror::Error)]
392pub enum ProjectError {
393 #[error("failed to determine directory containing project file {0}")]
395 Parent(PathBuf),
396
397 #[error("failed to make filename absolute: {0}")]
399 Canonicalize(PathBuf, #[source] std::io::Error),
400
401 #[error("failed top read project file {0}")]
403 Read(PathBuf, #[source] std::io::Error),
404
405 #[error("failed to parse project file as YAML: {0}")]
407 Yaml(PathBuf, #[source] serde_norway::Error),
408
409 #[error("failed to serialize project state as YAML: {0:#?}")]
411 SerializeState(State, #[source] serde_norway::Error),
412
413 #[error("failed to write project state to file {0}")]
415 WriteState(PathBuf, #[source] std::io::Error),
416
417 #[error("failed to read project state from file {0}")]
419 ReadState(PathBuf, #[source] std::io::Error),
420
421 #[error("failed to parse project state file as YAML: {0}")]
423 ParseState(PathBuf, #[source] serde_norway::Error),
424
425 #[error("failed to create project state directory {0}")]
427 CreateState(PathBuf, #[source] std::io::Error),
428
429 #[error("failed to remove run log file {0}")]
431 RemoveRunLog(PathBuf, #[source] std::io::Error),
432
433 #[error("failed to remove raw log file {0}")]
435 RemoveRawLog(PathBuf, #[source] std::io::Error),
436
437 #[error("failed to create run log file {0}")]
439 CreateRunLog(PathBuf, #[source] std::io::Error),
440
441 #[error("failed to append to run log file {0}")]
443 AppendToRunLog(PathBuf, #[source] std::io::Error),
444
445 #[error("failed to read run log file {0}")]
447 ReadRunLog(PathBuf, #[source] std::io::Error),
448
449 #[error("failed to remove console log file {0}")]
451 RemoveConsoleLog(PathBuf, #[source] std::io::Error),
452
453 #[error("failed to create consolelog file {0}")]
455 CreateConsoleLog(PathBuf, #[source] std::io::Error),
456
457 #[error("failed to append to console log file {0}")]
459 AppendToConsoleLog(PathBuf, #[source] std::io::Error),
460
461 #[error("failed to read console log file {0}")]
463 ReadConsoleLog(PathBuf, #[source] std::io::Error),
464
465 #[error(transparent)]
467 MKdir(#[from] crate::util::UtilError),
468
469 #[error("project {0} source directory is not a directory: {1}")]
471 NotADirectory(String, PathBuf),
472}