use crate::action::Action;
use crate::cache::Cache;
use crate::types::{ActionId, BuildError, BuildEvent, Ctx, Sha256};
use futures::future::{Either, join_all, select};
use std::collections::{HashMap, HashSet};
use std::pin::pin;
use thiserror::Error;
#[derive(Clone, Debug)]
pub struct Plan {
layers: Vec<Vec<ActionId>>,
reasons: HashMap<ActionId, ChangeReason>,
input_hashes: HashMap<ActionId, Sha256>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum ChangeReason {
NoCachedBuild,
InputHashChanged {
from: Sha256,
to: Sha256,
},
UpstreamMoved {
dep: ActionId,
},
}
#[derive(Debug, Error)]
pub enum PlanError {
#[error("cycle detected: {0:?}")]
Cycle(Vec<ActionId>),
#[error("action {from} depends on {to}, which is not registered")]
MissingDep {
from: ActionId,
to: ActionId,
},
#[error(transparent)]
Build(#[from] BuildError),
}
impl Plan {
#[must_use]
pub fn layers(&self) -> &[Vec<ActionId>] {
&self.layers
}
#[must_use]
pub fn reasons(&self) -> &HashMap<ActionId, ChangeReason> {
&self.reasons
}
pub fn stale(&self) -> impl Iterator<Item = &ActionId> {
self.reasons.keys()
}
pub fn flat_topo(&self) -> impl Iterator<Item = &ActionId> {
self.layers.iter().flatten()
}
pub async fn compute(
actions: &[Box<dyn Action>],
cache: &dyn Cache,
ctx: &Ctx,
) -> std::result::Result<Self, PlanError> {
let ids: HashSet<ActionId> = actions.iter().map(|a| a.id()).collect();
let mut inbound: HashMap<ActionId, usize> = ids.iter().map(|i| (i.clone(), 0)).collect();
let mut dependents: HashMap<ActionId, Vec<ActionId>> = HashMap::new();
for a in actions {
for dep in a.deps() {
if !ids.contains(&dep) {
return Err(PlanError::MissingDep {
from: a.id(),
to: dep,
});
}
if let Some(c) = inbound.get_mut(&a.id()) {
*c += 1;
}
dependents.entry(dep).or_default().push(a.id());
}
}
let mut layers: Vec<Vec<ActionId>> = Vec::new();
loop {
let mut layer: Vec<ActionId> = inbound
.iter()
.filter(|(_, c)| **c == 0)
.map(|(id, _)| id.clone())
.collect();
if layer.is_empty() {
break;
}
layer.sort_by(|a, b| a.0.cmp(&b.0)); for id in &layer {
inbound.remove(id);
if let Some(children) = dependents.get(id) {
for child in children.clone() {
if let Some(c) = inbound.get_mut(&child) {
*c -= 1;
}
}
}
}
layers.push(layer);
}
if !inbound.is_empty() {
let mut remaining: Vec<ActionId> = inbound.into_keys().collect();
remaining.sort_by(|a, b| a.0.cmp(&b.0));
return Err(PlanError::Cycle(remaining));
}
let by_id: HashMap<ActionId, &Box<dyn Action>> =
actions.iter().map(|a| (a.id(), a)).collect();
let mut reasons: HashMap<ActionId, ChangeReason> = HashMap::new();
let mut stale: HashSet<ActionId> = HashSet::new();
let mut input_hashes: HashMap<ActionId, Sha256> = HashMap::new();
for layer in &layers {
if ctx.cancel.is_cancelled() {
return Err(PlanError::Build(BuildError::Cancelled));
}
let action_probes: Vec<(ActionId, &dyn Action)> = layer
.iter()
.map(|id| (id.clone(), by_id[id].as_ref()))
.collect();
let hash_results: Vec<(ActionId, Result<Sha256, BuildError>)> =
join_all(action_probes.into_iter().map(|(id, action)| async move {
let probe = pin!(action.input_hash(ctx));
let cancel = pin!(ctx.cancel.cancelled());
let result = match select(probe, cancel).await {
Either::Left((r, _)) => r,
Either::Right(((), _)) => Err(BuildError::Cancelled),
};
(id, result)
}))
.await;
let cached: Vec<(ActionId, Option<BuildEvent>)> = join_all(layer.iter().map(|id| {
let id = id.clone();
async move { (id.clone(), cache.last_build(&id).await) }
}))
.await;
let mut hashes_now: HashMap<ActionId, Sha256> = HashMap::new();
for (id, result) in hash_results {
let now = result?;
hashes_now.insert(id, now);
}
let cached_map: HashMap<ActionId, Option<BuildEvent>> = cached.into_iter().collect();
for id in layer {
let now = hashes_now[id];
input_hashes.insert(id.clone(), now);
let last = cached_map.get(id).and_then(Option::as_ref);
if let Some(reason) = classify(id, by_id[id].as_ref(), now, last, &stale) {
reasons.insert(id.clone(), reason);
stale.insert(id.clone());
}
}
}
Ok(Self {
layers,
reasons,
input_hashes,
})
}
#[must_use]
pub fn input_hash(&self, id: &ActionId) -> Option<Sha256> {
self.input_hashes.get(id).copied()
}
}
fn classify(
id: &ActionId,
action: &dyn Action,
now: Sha256,
last: Option<&BuildEvent>,
stale: &HashSet<ActionId>,
) -> Option<ChangeReason> {
let _ = id; let Some(event) = last else {
return Some(ChangeReason::NoCachedBuild);
};
match event {
BuildEvent::Failed { .. } => Some(ChangeReason::NoCachedBuild),
BuildEvent::Success { input, .. } if *input != now => {
Some(ChangeReason::InputHashChanged {
from: *input,
to: now,
})
}
BuildEvent::Success { .. } => action
.deps()
.into_iter()
.find(|d| stale.contains(d))
.map(|dep| ChangeReason::UpstreamMoved { dep }),
}
}