#![allow(clippy::result_large_err)]
use std::{
collections::HashMap,
fmt::Debug,
path::{Path, PathBuf},
};
use serde::{Deserialize, Serialize};
use crate::{
action::{Context, PostPlanAction, PrePlanAction, RunnableAction, UnsafeAction},
config::Config,
project::{Project, State},
qemu,
runlog::RunLogSource,
};
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct Plan {
steps: Vec<UnsafeAction>,
}
impl Plan {
pub fn from_file(filename: &Path) -> Result<Self, PlanError> {
let plan = std::fs::read(filename).map_err(|e| PlanError::PlanOpen(filename.into(), e))?;
let plan = serde_norway::from_slice(&plan)
.map_err(|e| PlanError::PlanParse(filename.into(), e))?;
Ok(plan)
}
pub fn to_file(&self, filename: &Path) -> Result<(), PlanError> {
let plan = serde_norway::to_string(&self).map_err(PlanError::PlanSerialize)?;
std::fs::write(filename, plan).map_err(|e| PlanError::PlanWrite(filename.into(), e))?;
Ok(())
}
pub fn push(&mut self, action: UnsafeAction) {
self.steps.push(action);
}
pub fn iter(&self) -> impl Iterator<Item = &UnsafeAction> {
self.steps.iter()
}
}
#[derive(Debug, Default, Clone, Serialize, Deserialize, Eq, PartialEq)]
#[serde(deny_unknown_fields)]
pub struct RunnablePlan {
steps: Vec<RunnableAction>,
executor_drive: Option<String>,
source_drive: Option<String>,
artifact_drive: Option<String>,
cache_drive: Option<String>,
deps_drive: Option<String>,
workspace_dir: Option<String>,
source_dir: Option<String>,
deps_dir: Option<String>,
cache_dir: Option<String>,
artifacts_dir: Option<String>,
#[serde(default)]
envs: HashMap<String, Vec<u8>>,
}
impl RunnablePlan {
#[cfg(test)]
fn parse_str(yaml: &str) -> Result<Self, PlanError> {
serde_norway::from_str(yaml).map_err(PlanError::PlanParseStr)
}
pub fn from_file(filename: &Path) -> Result<Self, PlanError> {
let plan = std::fs::read(filename).map_err(|e| PlanError::PlanOpen(filename.into(), e))?;
let plan = String::from_utf8_lossy(&plan);
let plan: Self =
serde_norway::from_str(&plan).map_err(|e| PlanError::PlanParse(filename.into(), e))?;
for step in plan.steps.iter() {
if let RunnableAction::HttpGet(x) = step {
for item in x.items() {
if item
.filename()
.as_os_str()
.as_encoded_bytes()
.contains(&b'/')
{
return Err(PlanError::FilenameIsNotBasename(filename.to_path_buf()));
}
}
}
}
Ok(plan)
}
pub fn to_string(&self) -> Result<String, PlanError> {
serde_norway::to_string(self).map_err(PlanError::PlanSerialize)
}
pub fn to_file(&self, filename: &Path) -> Result<(), PlanError> {
let plan = serde_norway::to_string(&self).map_err(PlanError::PlanSerialize)?;
std::fs::write(filename, plan).map_err(|e| PlanError::PlanWrite(filename.into(), e))?;
Ok(())
}
pub fn envs(&self) -> &HashMap<String, Vec<u8>> {
&self.envs
}
pub fn push(&mut self, action: RunnableAction) {
self.steps.push(action);
}
pub fn push_unsafe_actions<'a>(&mut self, actions: impl Iterator<Item = &'a UnsafeAction>) {
for action in actions {
self.push(RunnableAction::from_unsafe_action(action));
}
}
pub fn carry_over_from_context(&mut self, context: &Context) {
for (k, v) in context.plan_envs().iter() {
self.envs.insert(k.clone(), v.to_vec());
}
}
pub fn iter(&self) -> impl Iterator<Item = &RunnableAction> {
self.steps.iter()
}
pub fn execute(&self, source: RunLogSource, context: &mut Context) -> Result<(), PlanError> {
context
.set_envs_from_plan(self)
.map_err(PlanError::Context)?;
for action in self.steps.iter() {
context.runlog().execute_action(source, action);
let result = action.execute(context);
match &result {
Ok(()) => context.runlog().action_succeeded(source, action),
Err(_) => {
context.runlog().action_failed(source, action);
result?;
}
}
}
if !self.steps.is_empty() {
context.runlog().plan_succeeded(source);
}
Ok(())
}
pub fn executor_drive(&self) -> Option<&String> {
self.executor_drive.as_ref()
}
pub fn source_drive(&self) -> Option<&String> {
self.source_drive.as_ref()
}
pub fn run_artifact_drive(&self) -> Option<&String> {
self.artifact_drive.as_ref()
}
pub fn cache_drive(&self) -> Option<&String> {
self.cache_drive.as_ref()
}
pub fn deps_drive(&self) -> Option<&String> {
self.deps_drive.as_ref()
}
pub fn workspace_dir(&self) -> Option<&String> {
self.workspace_dir.as_ref()
}
pub fn source_dir(&self) -> Option<&String> {
self.source_dir.as_ref()
}
pub fn deps_dir(&self) -> Option<&String> {
self.deps_dir.as_ref()
}
pub fn cache_dir(&self) -> Option<&String> {
self.cache_dir.as_ref()
}
pub fn artifacts_dir(&self) -> Option<&String> {
self.artifacts_dir.as_ref()
}
pub fn set_executor_drive(&mut self, path: &str) {
self.executor_drive = Some(path.into());
}
pub fn set_source_drive(&mut self, path: &str) {
self.source_drive = Some(path.into());
}
pub fn set_artifact_drive(&mut self, path: &str) {
self.artifact_drive = Some(path.into());
}
pub fn set_cache_drive(&mut self, path: &str) {
self.cache_drive = Some(path.into());
}
pub fn set_deps_drive(&mut self, path: &str) {
self.deps_drive = Some(path.into());
}
pub fn set_workspace_dir(&mut self, path: &str) {
self.workspace_dir = Some(path.into());
}
pub fn set_source_dir(&mut self, path: &str) {
self.source_dir = Some(path.into());
}
pub fn set_deps_dir(&mut self, path: &str) {
self.deps_dir = Some(path.into());
}
pub fn set_cache_dir(&mut self, path: &str) {
self.cache_dir = Some(path.into());
}
pub fn set_artifacts_dir(&mut self, path: &str) {
self.artifacts_dir = Some(path.into());
}
pub fn set_unset_dirs(&mut self, path: &str) {
fn set(s: &mut Option<String>, path: &str) {
if s.is_none() {
*s = Some(path.to_string());
}
}
set(&mut self.workspace_dir, path);
set(&mut self.source_dir, path);
set(&mut self.deps_dir, path);
set(&mut self.cache_dir, path);
set(&mut self.artifacts_dir, path);
}
}
pub fn construct_all_plans(
config: &Config,
project_name: &str,
project: &Project,
state: &State,
) -> Result<(RunnablePlan, RunnablePlan, RunnablePlan), PlanError> {
let pre_plan = runnable_plan_from_pre_plan_actions(project, state, project.pre_plan());
let plan = construct_runnable_plan(project.plan())?;
let post_plan = runnable_plan_from_post_plan_actions(
config,
project_name,
project,
state,
project.post_plan(),
);
Ok((pre_plan, plan, post_plan))
}
pub fn construct_runnable_plan(actions: &[UnsafeAction]) -> Result<RunnablePlan, PlanError> {
let prologue = [
UnsafeAction::mkdir(Path::new(qemu::WORKSPACE_DIR)),
UnsafeAction::mkdir(Path::new(qemu::ARTIFACTS_DIR)),
UnsafeAction::tar_extract(Path::new(qemu::SOURCE_DRIVE), Path::new(qemu::SOURCE_DIR)),
UnsafeAction::tar_extract(Path::new(qemu::DEPS_DRIVE), Path::new(qemu::DEPS_DIR)),
UnsafeAction::tar_extract(Path::new(qemu::CACHE_DRIVE), Path::new(qemu::CACHE_DIR)),
UnsafeAction::shell("ln -sf /ci /workspace"),
UnsafeAction::shell("git config --global user.name 'Ambient CI'"),
UnsafeAction::shell("git config --global user.email ambient@example.com"),
];
let epilogue = [
UnsafeAction::tar_create(Path::new(qemu::CACHE_DRIVE), Path::new(qemu::CACHE_DIR)),
UnsafeAction::tar_create(
Path::new(qemu::ARTIFACT_DRIVE),
Path::new(qemu::ARTIFACTS_DIR),
),
];
let mut runnable = RunnablePlan::default();
runnable.set_executor_drive(qemu::EXECUTOR_DRIVE);
runnable.set_source_drive(qemu::SOURCE_DRIVE);
runnable.set_artifact_drive(qemu::ARTIFACT_DRIVE);
runnable.set_cache_drive(qemu::CACHE_DRIVE);
runnable.set_deps_drive(qemu::DEPS_DRIVE);
runnable.set_workspace_dir(qemu::WORKSPACE_DIR);
runnable.set_source_dir(qemu::SOURCE_DIR);
runnable.set_artifacts_dir(qemu::ARTIFACTS_DIR);
runnable.set_deps_dir(qemu::DEPS_DIR);
runnable.set_cache_dir(qemu::CACHE_DIR);
runnable.push_unsafe_actions(prologue.iter());
runnable.push_unsafe_actions(actions.iter());
runnable.push_unsafe_actions(epilogue.iter());
Ok(runnable)
}
pub fn runnable_plan_from_pre_plan_actions(
project: &Project,
state: &State,
actions: &[PrePlanAction],
) -> RunnablePlan {
fn path(path: &Path) -> String {
path.to_string_lossy().into_owned()
}
let mut plan = RunnablePlan::default();
plan.set_cache_dir(&path(&state.cachedir()));
plan.set_deps_dir(&path(&state.dependenciesdir()));
plan.set_artifacts_dir(&path(&state.artifactsdir()));
plan.set_source_dir(&path(project.source()));
for action in actions {
plan.push(RunnableAction::from_pre_plan_action(action));
}
plan
}
pub fn runnable_plan_from_post_plan_actions(
config: &Config,
project_name: &str,
project: &Project,
state: &State,
actions: &[PostPlanAction],
) -> RunnablePlan {
fn path(path: &Path) -> String {
path.to_string_lossy().into_owned()
}
let mut plan = RunnablePlan::default();
plan.set_cache_dir(&path(&state.cachedir()));
plan.set_deps_dir(&path(&state.dependenciesdir()));
plan.set_artifacts_dir(&path(&state.artifactsdir()));
plan.set_source_dir(&path(project.source()));
for action in actions {
plan.push(RunnableAction::from_post_plan_action(
action,
config.rsync_target_for_project(project_name).as_deref(),
config.dput_target(),
));
}
plan
}
#[derive(Debug, thiserror::Error)]
pub enum PlanError {
#[error("failed to read CI plan file: {0}")]
PlanOpen(PathBuf, #[source] std::io::Error),
#[error("failed to parse CI plan file as YAML: {0}")]
PlanParse(PathBuf, #[source] serde_norway::Error),
#[error("failed to parse CI plan")]
PlanParseStr(#[source] serde_norway::Error),
#[error("failed to serialize CI plan as YAML")]
PlanSerialize(#[source] serde_norway::Error),
#[error("failed to write CI plan file: {0}")]
PlanWrite(PathBuf, #[source] std::io::Error),
#[error(transparent)]
Action(#[from] crate::action::ActionError),
#[error("the filename in a URL/filename pair contains a directory")]
FilenameIsNotBasename(PathBuf),
#[error("failed to create a context for executing actions")]
Context(#[source] crate::action::ActionError),
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn round_trip() -> Result<(), Box<dyn std::error::Error>> {
let mut plan = RunnablePlan::default();
plan.set_source_dir("/src");
let s = plan.to_string()?;
let des = RunnablePlan::parse_str(&s)?;
assert_eq!(plan, des);
Ok(())
}
}