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 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#[derive(Debug, Clone, Deserialize, Serialize)]
142#[allow(dead_code)]
143pub struct State {
144 #[serde(skip)]
146 filename: PathBuf,
147
148 #[serde(skip)]
150 statedir: PathBuf,
151
152 latest_commit: Option<String>,
154}
155
156impl State {
157 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 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 pub fn statedir(&self) -> &Path {
201 &self.statedir
202 }
203
204 pub fn artifactsdir(&self) -> PathBuf {
206 self.statedir.join("artifacts")
207 }
208
209 pub fn cachedir(&self) -> PathBuf {
211 self.statedir.join("cache")
212 }
213
214 pub fn dependenciesdir(&self) -> PathBuf {
216 self.statedir.join("dependencies")
217 }
218
219 pub fn latest_commit(&self) -> Option<&str> {
221 self.latest_commit.as_deref()
222 }
223
224 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 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 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 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 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}