mod execution;
mod handlers;
mod policy;
pub use policy::{BuildPolicy, IncrementalPolicy, ProductAction};
use anyhow::Result;
use indicatif::ProgressBar;
use parking_lot::Mutex;
use std::collections::{BTreeSet, HashMap, HashSet};
use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
use crate::color;
use crate::display::DisplayOptions;
use crate::errors;
use crate::graph::BuildGraph;
use crate::object_store::{ExplainAction, ObjectStore};
use crate::processor::ProcessorMap;
use crate::stats::ProcessStats;
enum PreCheckResult {
Handled,
NeedsExecution,
}
enum RestoreOutcome {
Restored,
Failed,
NotRestorable,
}
struct WorkItem {
product_id: usize,
product: crate::graph::Product,
input_checksum: String,
action: ProductAction,
}
pub type GraphRefresh<'r> = dyn FnMut(&mut BuildGraph, &[usize]) -> Result<bool> + 'r;
struct HandlerContext<'b> {
product: &'b crate::graph::Product,
id: usize,
input_checksum: &'b str,
proc_name: &'b str,
keep_going: bool,
shared: &'b SharedState,
pb: &'b ProgressBar,
}
struct LevelWork {
batch_groups: HashMap<String, Vec<WorkItem>>,
non_batch_items: Vec<WorkItem>,
}
#[derive(Debug)]
pub struct ExecutorOptions {
pub parallel: usize,
pub verbose: bool,
pub display_opts: DisplayOptions,
pub batch_size: Option<usize>,
pub explain: bool,
pub retry: usize,
pub force: bool,
pub keep_going: bool,
pub timings: bool,
}
#[derive(Debug)]
struct SharedState {
stats: Arc<Mutex<HashMap<String, ProcessStats>>>,
errors: Arc<Mutex<Vec<anyhow::Error>>>,
failed_products: Arc<Mutex<HashSet<usize>>>,
failed_messages: Arc<Mutex<Vec<String>>>,
failed_processors: Arc<Mutex<HashSet<String>>>,
changed: Arc<Mutex<Vec<usize>>>,
attempted: Arc<Mutex<HashSet<usize>>>,
global_current: Arc<AtomicUsize>,
global_total: usize,
}
pub struct ClassifiedProduct {
pub id: usize,
pub action: ProductAction,
pub input_checksum: String,
}
pub struct Classification {
pub skip_count: usize,
pub restore_count: usize,
pub build_count: usize,
pub products: Vec<ClassifiedProduct>,
}
pub fn classify_products(
ctx: &crate::build_context::BuildContext,
policy: &dyn BuildPolicy,
graph: &BuildGraph,
order: &[usize],
object_store: &ObjectStore,
force: bool,
) -> Classification {
let mut skip_count = 0;
let mut restore_count = 0;
let mut build_count = 0;
let mut will_change: HashSet<usize> = HashSet::new();
let mut products: Vec<ClassifiedProduct> = Vec::with_capacity(order.len());
for &id in order {
let product = graph.get_product(id).expect(errors::INVALID_PRODUCT_ID);
let dep_changed = graph
.get_dependencies(id)
.iter()
.any(|d| will_change.contains(d));
let Ok(input_checksum) = crate::checksum::combined_input_checksum(ctx, &product.inputs)
else {
build_count += 1;
will_change.insert(id);
products.push(ClassifiedProduct {
id,
action: ProductAction::Build,
input_checksum: String::new(),
});
continue;
};
let action = if dep_changed {
ProductAction::Build
} else {
policy.classify(ctx, product, object_store, &input_checksum, force)
};
match action {
ProductAction::Skip => {
skip_count += 1;
}
ProductAction::Restore => {
restore_count += 1;
will_change.insert(id);
}
ProductAction::Build => {
build_count += 1;
will_change.insert(id);
}
}
products.push(ClassifiedProduct {
id,
action,
input_checksum,
});
}
Classification {
skip_count,
restore_count,
build_count,
products,
}
}
pub struct Executor<'a> {
processors: &'a ProcessorMap,
build_ctx: &'a crate::build_context::BuildContext,
policy: &'a dyn BuildPolicy,
parallel: usize,
verbose: bool,
display_opts: DisplayOptions,
batch_size: Option<usize>,
explain: bool,
retry: usize,
force: bool,
keep_going: bool,
timings: bool,
}
impl<'a> Executor<'a> {
pub fn new(
processors: &'a ProcessorMap,
build_ctx: &'a crate::build_context::BuildContext,
policy: &'a dyn BuildPolicy,
opts: ExecutorOptions,
) -> Self {
Self {
processors,
build_ctx,
policy,
parallel: opts.parallel,
verbose: opts.verbose && crate::json_output::human_output_enabled(),
display_opts: opts.display_opts,
batch_size: opts.batch_size,
explain: opts.explain,
retry: opts.retry,
force: opts.force,
keep_going: opts.keep_going,
timings: opts.timings,
}
}
fn is_interrupted(&self) -> bool {
self.build_ctx.is_interrupted()
}
fn product_display(&self, product: &crate::graph::Product) -> String {
product.display(self.display_opts)
}
fn inc_global(shared: &SharedState) {
shared.global_current.fetch_add(1, Ordering::SeqCst);
}
fn inc_progress(pb: &ProgressBar, shared: &SharedState) {
Self::inc_global(shared);
pb.inc(1);
}
fn print_explain(&self, product: &crate::graph::Product, action: &ExplainAction) {
let styled = match action {
ExplainAction::Skip => color::dim("SKIP"),
ExplainAction::Restore(_) => color::cyan("RESTORE"),
ExplainAction::Rebuild(_) => color::yellow("BUILD"),
};
crate::output::info(&format!(
"[{}] {} {} ({})",
product.processor,
styled,
self.product_display(product),
action
));
}
pub fn clean(&self, graph: &BuildGraph, verbose: bool) -> Result<HashMap<String, usize>> {
let mut stats: HashMap<String, usize> = HashMap::new();
for product in graph.products() {
if self.is_interrupted() {
return Err(crate::exit_code::interrupted());
}
if let Some(processor) = self.processors.get(&product.processor) {
let count = processor.clean(product, verbose)?;
if count > 0 {
*stats.entry(product.processor.clone()).or_default() += count;
}
}
}
Ok(stats)
}
}
pub fn has_failed_dependency(graph: &BuildGraph, id: usize, failed: &HashSet<usize>) -> bool {
for &dep_id in graph.get_dependencies(id) {
if failed.contains(&dep_id) {
return true;
}
}
false
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum ProductState {
Waiting,
Dispatched,
Done,
}
struct ReadyTracker {
unfinished_deps: Vec<usize>,
state: Vec<ProductState>,
ready: BTreeSet<usize>,
in_flight: usize,
}
impl ReadyTracker {
fn new(graph: &BuildGraph) -> Self {
let count = graph.products().len();
let mut tracker = Self {
unfinished_deps: vec![0; count],
state: vec![ProductState::Waiting; count],
ready: BTreeSet::new(),
in_flight: 0,
};
tracker.recount(graph);
tracker
}
fn recount(&mut self, graph: &BuildGraph) {
self.ready.clear();
for id in 0..self.state.len() {
if self.state[id] != ProductState::Waiting {
continue;
}
self.unfinished_deps[id] = graph
.get_dependencies(id)
.iter()
.filter(|&&dep| self.state[dep] != ProductState::Done)
.count();
if self.unfinished_deps[id] == 0 {
self.ready.insert(id);
}
}
}
fn take_ready(&mut self) -> Vec<usize> {
let ready: Vec<usize> = std::mem::take(&mut self.ready).into_iter().collect();
for &id in &ready {
self.state[id] = ProductState::Dispatched;
}
self.in_flight += ready.len();
ready
}
fn finish(&mut self, graph: &BuildGraph, id: usize) {
debug_assert_eq!(self.state[id], ProductState::Dispatched);
self.state[id] = ProductState::Done;
self.in_flight -= 1;
for &dependent in graph.get_dependents(id) {
if self.state[dependent] == ProductState::Waiting {
self.unfinished_deps[dependent] -= 1;
if self.unfinished_deps[dependent] == 0 {
self.ready.insert(dependent);
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn ready_tracker_releases_dependents_without_a_barrier() {
let mut g = BuildGraph::new();
let top = g
.add_product(vec!["a.src".into()], vec!["a.o".into()], "cc", None)
.unwrap();
let left = g
.add_product(vec!["a.o".into()], vec!["b.o".into()], "cc", None)
.unwrap();
let right = g
.add_product(vec!["a.o".into()], vec!["c.o".into()], "cc", None)
.unwrap();
let bottom = g
.add_product(
vec!["b.o".into(), "c.o".into()],
vec!["d.o".into()],
"cc",
None,
)
.unwrap();
let slow = g
.add_product(vec!["x.src".into()], vec!["x.o".into()], "cc", None)
.unwrap();
g.resolve_dependencies();
let mut tracker = ReadyTracker::new(&g);
assert_eq!(tracker.take_ready(), vec![top, slow]);
tracker.finish(&g, top);
assert_eq!(tracker.take_ready(), vec![left, right], "slow still runs");
tracker.finish(&g, left);
assert_eq!(
tracker.take_ready(),
Vec::<usize>::new(),
"bottom needs right"
);
tracker.finish(&g, right);
assert_eq!(tracker.take_ready(), vec![bottom]);
tracker.finish(&g, bottom);
assert_eq!(tracker.in_flight, 1, "slow is still in flight");
tracker.finish(&g, slow);
assert_eq!(tracker.in_flight, 0);
assert_eq!(tracker.take_ready(), Vec::<usize>::new());
}
#[test]
fn ready_tracker_recount_respects_new_edges() {
let mut g = BuildGraph::new();
let producer = g
.add_product(vec!["gen.src".into()], vec!["gen.h".into()], "gen", None)
.unwrap();
let blocker = g
.add_product(vec!["b.src".into()], vec!["b.o".into()], "cc", None)
.unwrap();
let consumer = g
.add_product(vec!["main.c".into()], vec!["main.o".into()], "cc", None)
.unwrap();
g.resolve_dependencies();
let mut tracker = ReadyTracker::new(&g);
assert_eq!(
tracker.ready.iter().copied().collect::<Vec<_>>(),
vec![producer, blocker, consumer]
);
g.add_inputs(consumer, &["gen.h".into()]);
g.resolve_dependencies();
tracker.recount(&g);
assert_eq!(
tracker.take_ready(),
vec![producer, blocker],
"consumer waits for gen.h"
);
tracker.finish(&g, producer);
assert_eq!(tracker.take_ready(), vec![consumer]);
}
#[test]
fn classify_predicts_build_downstream_of_a_change() {
let tmp = tempfile::TempDir::new().unwrap();
let store = ObjectStore::new_in(tmp.path());
let ctx = crate::build_context::BuildContext::new();
ctx.set_mtime_check(false);
let src = tmp.path().join("a.src");
let mid = tmp.path().join("a.mid");
std::fs::write(&src, "a").unwrap();
std::fs::write(&mid, "m").unwrap();
let mut g = BuildGraph::new();
let producer = g
.add_product(vec![src], vec![mid.clone()], "gen", None)
.unwrap();
let checker = g
.add_product(vec![mid.clone()], vec![], "check", None)
.unwrap();
g.resolve_dependencies();
let mid_checksum =
crate::checksum::combined_input_checksum(&ctx, std::slice::from_ref(&mid)).unwrap();
let marker_key = g
.get_product(checker)
.unwrap()
.descriptor_key(&mid_checksum);
store.store_marker(&ctx, &marker_key).unwrap();
let order = g.topological_sort().unwrap();
let classification = classify_products(&ctx, &IncrementalPolicy, &g, &order, &store, false);
let action_of = |id: usize| {
classification
.products
.iter()
.find(|c| c.id == id)
.map(|c| c.action)
};
assert_eq!(action_of(producer), Some(ProductAction::Build));
assert_eq!(action_of(checker), Some(ProductAction::Build));
assert_eq!(
IncrementalPolicy.classify(
&ctx,
g.get_product(checker).unwrap(),
&store,
&mid_checksum,
false
),
ProductAction::Skip,
"the policy alone, on unchanged inputs, would skip it"
);
}
#[test]
fn ready_tracker_hands_out_every_product_once() {
let mut g = BuildGraph::new();
g.add_product(vec!["a.src".into()], vec!["a.o".into()], "cc", None)
.unwrap();
g.add_product(vec!["a.o".into()], vec!["b.o".into()], "cc", None)
.unwrap();
g.add_product(vec!["free.src".into()], vec!["free.o".into()], "cc", None)
.unwrap();
g.resolve_dependencies();
let mut tracker = ReadyTracker::new(&g);
let mut handed_out: Vec<usize> = Vec::new();
loop {
let wave = tracker.take_ready();
if wave.is_empty() {
break;
}
for &id in &wave {
tracker.finish(&g, id);
}
handed_out.extend(wave);
}
handed_out.sort_unstable();
assert_eq!(handed_out, vec![0, 1, 2]);
}
#[test]
fn failed_dependency_is_direct_only() {
let mut g = BuildGraph::new();
let a = g
.add_product(vec!["a.src".into()], vec!["a.o".into()], "cc", None)
.unwrap();
let b = g
.add_product(vec!["a.o".into()], vec!["b.o".into()], "cc", None)
.unwrap();
let c = g
.add_product(vec!["b.o".into()], vec!["c.o".into()], "cc", None)
.unwrap();
g.resolve_dependencies();
let failed: HashSet<usize> = [a].into();
assert!(
has_failed_dependency(&g, b, &failed),
"b directly depends on failed a"
);
assert!(
!has_failed_dependency(&g, c, &failed),
"c depends on a only through b; direct check must not see it"
);
assert!(
!has_failed_dependency(&g, a, &failed),
"a has no dependencies"
);
}
}