1#![allow(clippy::result_large_err)]
4
5use std::{
6 collections::HashMap,
7 fmt::Debug,
8 path::{Path, PathBuf},
9};
10
11use serde::{Deserialize, Serialize};
12
13use crate::{
14 action::{Context, PostPlanAction, PrePlanAction, RunnableAction, UnsafeAction},
15 config::Config,
16 project::{Project, State},
17 qemu,
18 runlog::RunLogSource,
19};
20
21#[derive(Debug, Clone, Default, Serialize, Deserialize)]
24pub struct Plan {
25 steps: Vec<UnsafeAction>,
26}
27
28impl Plan {
29 pub fn from_file(filename: &Path) -> Result<Self, PlanError> {
31 let plan = std::fs::read(filename).map_err(|e| PlanError::PlanOpen(filename.into(), e))?;
32 let plan = serde_norway::from_slice(&plan)
33 .map_err(|e| PlanError::PlanParse(filename.into(), e))?;
34 Ok(plan)
35 }
36
37 pub fn to_file(&self, filename: &Path) -> Result<(), PlanError> {
39 let plan = serde_norway::to_string(&self).map_err(PlanError::PlanSerialize)?;
40 std::fs::write(filename, plan).map_err(|e| PlanError::PlanWrite(filename.into(), e))?;
41 Ok(())
42 }
43
44 pub fn push(&mut self, action: UnsafeAction) {
46 self.steps.push(action);
47 }
48
49 pub fn iter(&self) -> impl Iterator<Item = &UnsafeAction> {
51 self.steps.iter()
52 }
53}
54
55#[derive(Debug, Default, Clone, Serialize, Deserialize, Eq, PartialEq)]
58#[serde(deny_unknown_fields)]
59pub struct RunnablePlan {
60 steps: Vec<RunnableAction>,
61 executor_drive: Option<String>,
62 source_drive: Option<String>,
63 artifact_drive: Option<String>,
64 cache_drive: Option<String>,
65 deps_drive: Option<String>,
66 workspace_dir: Option<String>,
67 source_dir: Option<String>,
68 deps_dir: Option<String>,
69 cache_dir: Option<String>,
70 artifacts_dir: Option<String>,
71
72 #[serde(default)]
73 envs: HashMap<String, Vec<u8>>,
74}
75
76impl RunnablePlan {
77 #[cfg(test)]
78 fn parse_str(yaml: &str) -> Result<Self, PlanError> {
79 serde_norway::from_str(yaml).map_err(PlanError::PlanParseStr)
80 }
81
82 pub fn from_file(filename: &Path) -> Result<Self, PlanError> {
84 let plan = std::fs::read(filename).map_err(|e| PlanError::PlanOpen(filename.into(), e))?;
85 let plan = String::from_utf8_lossy(&plan);
86 let plan: Self =
87 serde_norway::from_str(&plan).map_err(|e| PlanError::PlanParse(filename.into(), e))?;
88
89 for step in plan.steps.iter() {
90 if let RunnableAction::HttpGet(x) = step {
91 for item in x.items() {
92 if item
93 .filename()
94 .as_os_str()
95 .as_encoded_bytes()
96 .contains(&b'/')
97 {
98 return Err(PlanError::FilenameIsNotBasename(filename.to_path_buf()));
99 }
100 }
101 }
102 }
103
104 Ok(plan)
105 }
106
107 pub fn to_string(&self) -> Result<String, PlanError> {
109 serde_norway::to_string(self).map_err(PlanError::PlanSerialize)
110 }
111
112 pub fn to_file(&self, filename: &Path) -> Result<(), PlanError> {
114 let plan = serde_norway::to_string(&self).map_err(PlanError::PlanSerialize)?;
115 std::fs::write(filename, plan).map_err(|e| PlanError::PlanWrite(filename.into(), e))?;
116 Ok(())
117 }
118
119 pub fn envs(&self) -> &HashMap<String, Vec<u8>> {
121 &self.envs
122 }
123
124 pub fn push(&mut self, action: RunnableAction) {
126 self.steps.push(action);
127 }
128
129 pub fn push_unsafe_actions<'a>(&mut self, actions: impl Iterator<Item = &'a UnsafeAction>) {
131 for action in actions {
132 self.push(RunnableAction::from_unsafe_action(action));
133 }
134 }
135
136 pub fn carry_over_from_context(&mut self, context: &Context) {
138 for (k, v) in context.plan_envs().iter() {
139 self.envs.insert(k.clone(), v.to_vec());
140 }
141 }
142
143 pub fn iter(&self) -> impl Iterator<Item = &RunnableAction> {
145 self.steps.iter()
146 }
147
148 pub fn execute(&self, source: RunLogSource, context: &mut Context) -> Result<(), PlanError> {
150 context
151 .set_envs_from_plan(self)
152 .map_err(PlanError::Context)?;
153 for action in self.steps.iter() {
154 context.runlog().execute_action(source, action);
155 let result = action.execute(context);
156 match &result {
157 Ok(()) => context.runlog().action_succeeded(source, action),
158 Err(_) => {
159 context.runlog().action_failed(source, action);
160 result?;
168 }
169 }
170 }
171
172 if !self.steps.is_empty() {
173 context.runlog().plan_succeeded(source);
174 }
175 Ok(())
176 }
177
178 pub fn executor_drive(&self) -> Option<&String> {
180 self.executor_drive.as_ref()
181 }
182
183 pub fn source_drive(&self) -> Option<&String> {
185 self.source_drive.as_ref()
186 }
187
188 pub fn run_artifact_drive(&self) -> Option<&String> {
190 self.artifact_drive.as_ref()
191 }
192
193 pub fn cache_drive(&self) -> Option<&String> {
195 self.cache_drive.as_ref()
196 }
197
198 pub fn deps_drive(&self) -> Option<&String> {
200 self.deps_drive.as_ref()
201 }
202
203 pub fn workspace_dir(&self) -> Option<&String> {
205 self.workspace_dir.as_ref()
206 }
207
208 pub fn source_dir(&self) -> Option<&String> {
210 self.source_dir.as_ref()
211 }
212
213 pub fn deps_dir(&self) -> Option<&String> {
215 self.deps_dir.as_ref()
216 }
217
218 pub fn cache_dir(&self) -> Option<&String> {
220 self.cache_dir.as_ref()
221 }
222
223 pub fn artifacts_dir(&self) -> Option<&String> {
225 self.artifacts_dir.as_ref()
226 }
227
228 pub fn set_executor_drive(&mut self, path: &str) {
230 self.executor_drive = Some(path.into());
231 }
232
233 pub fn set_source_drive(&mut self, path: &str) {
235 self.source_drive = Some(path.into());
236 }
237
238 pub fn set_artifact_drive(&mut self, path: &str) {
240 self.artifact_drive = Some(path.into());
241 }
242
243 pub fn set_cache_drive(&mut self, path: &str) {
245 self.cache_drive = Some(path.into());
246 }
247
248 pub fn set_deps_drive(&mut self, path: &str) {
250 self.deps_drive = Some(path.into());
251 }
252
253 pub fn set_workspace_dir(&mut self, path: &str) {
255 self.workspace_dir = Some(path.into());
256 }
257
258 pub fn set_source_dir(&mut self, path: &str) {
260 self.source_dir = Some(path.into());
261 }
262
263 pub fn set_deps_dir(&mut self, path: &str) {
265 self.deps_dir = Some(path.into());
266 }
267
268 pub fn set_cache_dir(&mut self, path: &str) {
270 self.cache_dir = Some(path.into());
271 }
272
273 pub fn set_artifacts_dir(&mut self, path: &str) {
275 self.artifacts_dir = Some(path.into());
276 }
277
278 pub fn set_unset_dirs(&mut self, path: &str) {
280 fn set(s: &mut Option<String>, path: &str) {
281 if s.is_none() {
282 *s = Some(path.to_string());
283 }
284 }
285
286 set(&mut self.workspace_dir, path);
287 set(&mut self.source_dir, path);
288 set(&mut self.deps_dir, path);
289 set(&mut self.cache_dir, path);
290 set(&mut self.artifacts_dir, path);
291 }
292}
293
294pub fn construct_all_plans(
296 config: &Config,
297 project_name: &str,
298 project: &Project,
299 state: &State,
300) -> Result<(RunnablePlan, RunnablePlan, RunnablePlan), PlanError> {
301 let pre_plan = runnable_plan_from_pre_plan_actions(project, state, project.pre_plan());
302 let plan = construct_runnable_plan(project.plan())?;
303 let post_plan = runnable_plan_from_post_plan_actions(
304 config,
305 project_name,
306 project,
307 state,
308 project.post_plan(),
309 );
310 Ok((pre_plan, plan, post_plan))
311}
312
313pub fn construct_runnable_plan(actions: &[UnsafeAction]) -> Result<RunnablePlan, PlanError> {
315 let prologue = [
316 UnsafeAction::mkdir(Path::new(qemu::WORKSPACE_DIR)),
317 UnsafeAction::mkdir(Path::new(qemu::ARTIFACTS_DIR)),
318 UnsafeAction::tar_extract(Path::new(qemu::SOURCE_DRIVE), Path::new(qemu::SOURCE_DIR)),
319 UnsafeAction::tar_extract(Path::new(qemu::DEPS_DRIVE), Path::new(qemu::DEPS_DIR)),
320 UnsafeAction::tar_extract(Path::new(qemu::CACHE_DRIVE), Path::new(qemu::CACHE_DIR)),
321 UnsafeAction::shell("ln -sf /ci /workspace"),
322 UnsafeAction::shell("git config --global user.name 'Ambient CI'"),
323 UnsafeAction::shell("git config --global user.email ambient@example.com"),
324 ];
325
326 let epilogue = [
327 UnsafeAction::tar_create(Path::new(qemu::CACHE_DRIVE), Path::new(qemu::CACHE_DIR)),
328 UnsafeAction::tar_create(
329 Path::new(qemu::ARTIFACT_DRIVE),
330 Path::new(qemu::ARTIFACTS_DIR),
331 ),
332 ];
333
334 let mut runnable = RunnablePlan::default();
335 runnable.set_executor_drive(qemu::EXECUTOR_DRIVE);
336 runnable.set_source_drive(qemu::SOURCE_DRIVE);
337 runnable.set_artifact_drive(qemu::ARTIFACT_DRIVE);
338 runnable.set_cache_drive(qemu::CACHE_DRIVE);
339 runnable.set_deps_drive(qemu::DEPS_DRIVE);
340 runnable.set_workspace_dir(qemu::WORKSPACE_DIR);
341 runnable.set_source_dir(qemu::SOURCE_DIR);
342 runnable.set_artifacts_dir(qemu::ARTIFACTS_DIR);
343 runnable.set_deps_dir(qemu::DEPS_DIR);
344 runnable.set_cache_dir(qemu::CACHE_DIR);
345
346 runnable.push_unsafe_actions(prologue.iter());
347 runnable.push_unsafe_actions(actions.iter());
348 runnable.push_unsafe_actions(epilogue.iter());
349
350 Ok(runnable)
351}
352
353pub fn runnable_plan_from_pre_plan_actions(
355 project: &Project,
356 state: &State,
357 actions: &[PrePlanAction],
358) -> RunnablePlan {
359 fn path(path: &Path) -> String {
360 path.to_string_lossy().into_owned()
361 }
362
363 let mut plan = RunnablePlan::default();
364 plan.set_cache_dir(&path(&state.cachedir()));
365 plan.set_deps_dir(&path(&state.dependenciesdir()));
366 plan.set_artifacts_dir(&path(&state.artifactsdir()));
367 plan.set_source_dir(&path(project.source()));
368 for action in actions {
369 plan.push(RunnableAction::from_pre_plan_action(action));
370 }
371 plan
372}
373
374pub fn runnable_plan_from_post_plan_actions(
376 config: &Config,
377 project_name: &str,
378 project: &Project,
379 state: &State,
380 actions: &[PostPlanAction],
381) -> RunnablePlan {
382 fn path(path: &Path) -> String {
383 path.to_string_lossy().into_owned()
384 }
385
386 let mut plan = RunnablePlan::default();
387 plan.set_cache_dir(&path(&state.cachedir()));
388 plan.set_deps_dir(&path(&state.dependenciesdir()));
389 plan.set_artifacts_dir(&path(&state.artifactsdir()));
390 plan.set_source_dir(&path(project.source()));
391 for action in actions {
392 plan.push(RunnableAction::from_post_plan_action(
393 action,
394 config.rsync_target_for_project(project_name).as_deref(),
395 config.dput_target(),
396 ));
397 }
398 plan
399}
400
401#[derive(Debug, thiserror::Error)]
403pub enum PlanError {
404 #[error("failed to read CI plan file: {0}")]
406 PlanOpen(PathBuf, #[source] std::io::Error),
407
408 #[error("failed to parse CI plan file as YAML: {0}")]
410 PlanParse(PathBuf, #[source] serde_norway::Error),
411
412 #[error("failed to parse CI plan")]
414 PlanParseStr(#[source] serde_norway::Error),
415
416 #[error("failed to serialize CI plan as YAML")]
418 PlanSerialize(#[source] serde_norway::Error),
419
420 #[error("failed to write CI plan file: {0}")]
422 PlanWrite(PathBuf, #[source] std::io::Error),
423
424 #[error(transparent)]
426 Action(#[from] crate::action::ActionError),
427
428 #[error("the filename in a URL/filename pair contains a directory")]
430 FilenameIsNotBasename(PathBuf),
431
432 #[error("failed to create a context for executing actions")]
434 Context(#[source] crate::action::ActionError),
435}
436
437#[cfg(test)]
438mod test {
439 use super::*;
440
441 #[test]
442 fn round_trip() -> Result<(), Box<dyn std::error::Error>> {
443 let mut plan = RunnablePlan::default();
444 plan.set_source_dir("/src");
445
446 let s = plan.to_string()?;
447 let des = RunnablePlan::parse_str(&s)?;
448
449 assert_eq!(plan, des);
450 Ok(())
451 }
452}