| //! Build runner, choosing and executing tasks as determined by out of date inputs. |
| |
| use crate::{ |
| canon::canon_path, db, densemap::DenseMap, graph::*, hash, process, progress, |
| progress::Progress, signal, smallmap::SmallMap, task, trace, |
| }; |
| use std::collections::HashSet; |
| use std::collections::VecDeque; |
| |
| /// Build steps go through this sequence of states. |
| /// See "Build states" in the design notes. |
| #[derive(Clone, Copy, Debug, PartialEq)] |
| pub enum BuildState { |
| /// Default initial state, for Builds unneeded by the current build. |
| Unknown, |
| /// Builds we want to ensure are up to date, but which aren't ready yet. |
| Want, |
| /// Builds whose dependencies are up to date and are ready to be |
| /// checked. This is purely a function of whether all builds before |
| /// it have have run, and is independent of any file state. |
| /// |
| /// Preconditions: |
| /// - generated inputs: have already been stat()ed as part of completing |
| /// the step that generated those inputs |
| /// - non-generated inputs: may not have yet stat()ed, so doing those |
| /// stat()s is part of the work of running these builds |
| Ready, |
| /// Builds who have been determined not up to date and which are ready |
| /// to be executed. |
| Queued, |
| /// Currently executing. |
| Running, |
| /// Finished executing successfully. |
| Done, |
| /// Finished executing but failed. |
| Failed, |
| } |
| |
| /// Counters that track builds in each state, excluding phony builds. |
| /// This is only for display to the user and should not be used as a source of |
| /// truth for tracking progress. |
| /// Only covers builds not in the "unknown" state, which means it's only builds |
| /// that are considered part of the current build. |
| #[derive(Clone, Debug, Default)] |
| pub struct StateCounts([usize; 6]); |
| impl StateCounts { |
| fn idx(state: BuildState) -> usize { |
| match state { |
| BuildState::Unknown => panic!("unexpected state"), |
| BuildState::Want => 0, |
| BuildState::Ready => 1, |
| BuildState::Queued => 2, |
| BuildState::Running => 3, |
| BuildState::Done => 4, |
| BuildState::Failed => 5, |
| } |
| } |
| pub fn add(&mut self, state: BuildState, delta: isize) { |
| self.0[StateCounts::idx(state)] = |
| (self.0[StateCounts::idx(state)] as isize + delta) as usize; |
| } |
| pub fn get(&self, state: BuildState) -> usize { |
| self.0[StateCounts::idx(state)] |
| } |
| pub fn total(&self) -> usize { |
| self.0[0] + self.0[1] + self.0[2] + self.0[3] + self.0[4] + self.0[5] |
| } |
| } |
| |
| /// Pools gather collections of running builds. |
| /// Each running build is running "in" a pool; there's a default unbounded |
| /// pool for builds that don't specify one. |
| /// See "Tracking build state" in the design notes. |
| struct PoolState { |
| /// A queue of builds that are ready to be executed in this pool. |
| queued: VecDeque<BuildId>, |
| /// The number of builds currently running in this pool. |
| running: usize, |
| /// The total depth of the pool. 0 means unbounded. |
| depth: usize, |
| } |
| |
| impl PoolState { |
| fn new(depth: usize) -> Self { |
| PoolState { |
| queued: VecDeque::new(), |
| running: 0, |
| depth, |
| } |
| } |
| } |
| |
| /// BuildStates tracks progress of each Build step through the build. |
| /// See "Tracking build state" in the design notes. |
| struct BuildStates { |
| states: DenseMap<BuildId, BuildState>, |
| |
| /// Counts of builds in each state. |
| counts: StateCounts, |
| |
| /// Total number of builds that haven't been driven to completion |
| /// (done or failed). |
| total_pending: usize, |
| |
| /// Builds in the ready state, stored redundantly for quick access. |
| ready: VecDeque<BuildId>, |
| |
| /// Named pools of queued and running builds. |
| /// Builds otherwise default to using an unnamed infinite pool. |
| pools: SmallMap<String, PoolState>, |
| } |
| |
| impl BuildStates { |
| fn new(size: BuildId, depths: SmallMap<String, usize>) -> Self { |
| let mut pools = SmallMap::default(); |
| // The implied default pool. |
| pools.insert(String::from(""), PoolState::new(0)); |
| // TODO: the console pool is just a depth-1 pool for now. |
| pools.insert(String::from("console"), PoolState::new(1)); |
| for (name, depth) in depths.into_iter() { |
| pools.insert(name, PoolState::new(depth)); |
| } |
| BuildStates { |
| states: DenseMap::new_sized(size, BuildState::Unknown), |
| counts: StateCounts::default(), |
| total_pending: 0, |
| ready: VecDeque::new(), |
| pools, |
| } |
| } |
| |
| fn get(&self, id: BuildId) -> BuildState { |
| self.states[id] |
| } |
| |
| fn set(&mut self, id: BuildId, build: &Build, state: BuildState) { |
| // This function is called on all state transitions. |
| // We get 'prev', the previous state, and 'state', the new state. |
| let prev = std::mem::replace(&mut self.states[id], state); |
| |
| // We skip user-facing counters for phony builds. |
| let skip_ui_count = build.cmdline.is_none(); |
| |
| // println!("{:?} {:?}=>{:?} {:?}", id, prev, state, self.counts); |
| if prev == BuildState::Unknown { |
| self.total_pending += 1; |
| } else { |
| if prev == BuildState::Running { |
| self.get_pool(build).unwrap().running -= 1; |
| } |
| if !skip_ui_count { |
| self.counts.add(prev, -1); |
| } |
| } |
| |
| match state { |
| BuildState::Ready => { |
| self.ready.push_back(id); |
| } |
| BuildState::Running => { |
| // Trace instants render poorly in the old Chrome UI, and |
| // not at all in speedscope or Perfetto. |
| // if self.counts.get(BuildState::Running) == 0 { |
| // trace::if_enabled(|t| t.write_instant("first build")); |
| // } |
| self.get_pool(build).unwrap().running += 1; |
| } |
| BuildState::Done | BuildState::Failed => { |
| self.total_pending -= 1; |
| } |
| _ => {} |
| }; |
| if !skip_ui_count { |
| self.counts.add(state, 1); |
| } |
| |
| /* |
| This is too expensive to log on every individual state change... |
| trace::if_enabled(|t| { |
| t.write_counts( |
| "builds", |
| [ |
| ("want", self.counts.get(BuildState::Want)), |
| ("ready", self.counts.get(BuildState::Ready)), |
| ("queued", self.counts.get(BuildState::Queued)), |
| ("running", self.counts.get(BuildState::Running)), |
| ("done", self.counts.get(BuildState::Done)), |
| ] |
| .iter(), |
| ) |
| });*/ |
| } |
| |
| fn unfinished(&self) -> bool { |
| self.total_pending > 0 |
| } |
| |
| /// Visits a BuildId that is an input to the desired output. |
| /// Will recursively visit its own inputs. |
| fn want_build( |
| &mut self, |
| graph: &Graph, |
| stack: &mut Vec<FileId>, |
| id: BuildId, |
| ) -> anyhow::Result<()> { |
| if self.get(id) != BuildState::Unknown { |
| return Ok(()); // Already visited. |
| } |
| |
| let build = &graph.builds[id]; |
| self.set(id, build, BuildState::Want); |
| |
| // Any Build that doesn't depend on an output of another Build is ready. |
| let mut ready = true; |
| for &id in build.ordering_ins() { |
| self.want_file(graph, stack, id)?; |
| ready = ready && graph.file(id).input.is_none(); |
| } |
| for &id in build.validation_ins() { |
| // This build doesn't technically depend on the validation inputs, so |
| // allocate a new stack. Validation inputs could in theory depend on this build's |
| // outputs. |
| let mut stack = Vec::new(); |
| self.want_file(graph, &mut stack, id)?; |
| } |
| |
| if ready { |
| self.set(id, build, BuildState::Ready); |
| } |
| Ok(()) |
| } |
| |
| /// Visits a FileId that is an input to the desired output. |
| /// Will recursively visit its own inputs. |
| pub fn want_file( |
| &mut self, |
| graph: &Graph, |
| stack: &mut Vec<FileId>, |
| id: FileId, |
| ) -> anyhow::Result<()> { |
| // Check for a dependency cycle. |
| if let Some(cycle) = stack.iter().position(|&sid| sid == id) { |
| let mut err = "dependency cycle: ".to_string(); |
| for &id in stack[cycle..].iter() { |
| err.push_str(&format!("{} -> ", graph.file(id).name)); |
| } |
| err.push_str(&graph.file(id).name); |
| anyhow::bail!(err); |
| } |
| |
| if let Some(bid) = graph.file(id).input { |
| stack.push(id); |
| self.want_build(graph, stack, bid)?; |
| stack.pop(); |
| } |
| Ok(()) |
| } |
| |
| pub fn pop_ready(&mut self) -> Option<BuildId> { |
| // Here is where we might consider prioritizing from among the available |
| // ready set. |
| self.ready.pop_front() |
| } |
| |
| /// Look up a PoolState by name. |
| fn get_pool(&mut self, build: &Build) -> Option<&mut PoolState> { |
| let name = build.pool.as_deref().unwrap_or(""); |
| for (key, pool) in self.pools.iter_mut() { |
| if key == name { |
| return Some(pool); |
| } |
| } |
| None |
| } |
| |
| /// Mark a build as ready to run. |
| /// May fail if the build references an unknown pool. |
| pub fn enqueue(&mut self, id: BuildId, build: &Build) -> anyhow::Result<()> { |
| self.set(id, build, BuildState::Queued); |
| let pool = self.get_pool(build).ok_or_else(|| { |
| anyhow::anyhow!( |
| "{}: unknown pool {:?}", |
| build.location, |
| // Unnamed pool lookups always succeed, this error is about |
| // named pools. |
| build.pool.as_ref().unwrap() |
| ) |
| })?; |
| pool.queued.push_back(id); |
| Ok(()) |
| } |
| |
| /// Pop a ready to run queued build. |
| pub fn pop_queued(&mut self) -> Option<BuildId> { |
| for (_, pool) in self.pools.iter_mut() { |
| if pool.depth == 0 || pool.running < pool.depth { |
| if let Some(id) = pool.queued.pop_front() { |
| return Some(id); |
| } |
| } |
| } |
| None |
| } |
| } |
| |
| #[derive(Clone)] |
| pub struct Options { |
| pub failures_left: Option<usize>, |
| pub parallelism: usize, |
| /// When true, verbosely explain why targets are considered dirty. |
| pub explain: bool, |
| /// When true, just mark targets up to date without running anything. |
| pub adopt: bool, |
| } |
| |
| pub struct Work<'a> { |
| graph: Graph, |
| db: db::Writer, |
| pub progress: &'a mut dyn Progress, |
| options: Options, |
| file_state: FileState, |
| last_hashes: Hashes, |
| build_states: BuildStates, |
| } |
| |
| impl<'a> Work<'a> { |
| pub fn new( |
| graph: Graph, |
| last_hashes: Hashes, |
| db: db::Writer, |
| options: &Options, |
| progress: &'a mut dyn Progress, |
| pools: SmallMap<String, usize>, |
| ) -> Self { |
| let file_state = FileState::new(&graph); |
| let build_count = graph.builds.next_id(); |
| Work { |
| graph, |
| db, |
| progress, |
| options: options.clone(), |
| file_state, |
| last_hashes, |
| build_states: BuildStates::new(build_count, pools), |
| } |
| } |
| |
| pub fn lookup(&mut self, name: &str) -> Option<FileId> { |
| self.graph.files.lookup(&canon_path(name)) |
| } |
| |
| pub fn want_file(&mut self, id: FileId) -> anyhow::Result<()> { |
| let mut stack = Vec::new(); |
| self.build_states.want_file(&self.graph, &mut stack, id) |
| } |
| |
| pub fn want_every_file(&mut self, exclude: Option<FileId>) -> anyhow::Result<()> { |
| for id in self.graph.files.all_ids() { |
| if let Some(exclude) = exclude { |
| if id == exclude { |
| continue; |
| } |
| } |
| self.want_file(id)?; |
| } |
| Ok(()) |
| } |
| |
| /// Check whether a given build is ready, generally after one of its inputs |
| /// has been updated. |
| fn recheck_ready(&self, id: BuildId) -> bool { |
| let build = &self.graph.builds[id]; |
| // println!("recheck {:?} {} ({}...)", id, build.location, self.graph.file(build.outs()[0]).name); |
| for &id in build.ordering_ins() { |
| let file = self.graph.file(id); |
| match file.input { |
| None => { |
| // Only generated inputs contribute to readiness. |
| continue; |
| } |
| Some(id) => { |
| if self.build_states.get(id) != BuildState::Done { |
| // println!(" {:?} {} not done, it's {:?}", id, file.name, self.build_states.get(id)); |
| return false; |
| } |
| } |
| } |
| } |
| // println!("{:?} now ready", id); |
| true |
| } |
| |
| /// Return the id of any input file to a ready build step that is missing. |
| /// Assumes the input dependencies have already executed, but otherwise |
| /// may stat the file on disk. |
| fn ensure_input_files( |
| &mut self, |
| id: BuildId, |
| discovered: bool, |
| ) -> anyhow::Result<Option<FileId>> { |
| let build = &self.graph.builds[id]; |
| let ids = if discovered { |
| build.discovered_ins() |
| } else { |
| build.dirtying_ins() |
| }; |
| for &id in ids { |
| let mtime = match self.file_state.get(id) { |
| Some(mtime) => mtime, |
| None => { |
| let file = self.graph.file(id); |
| if file.input.is_some() { |
| // This dep is generated by some other build step, but the |
| // build graph didn't cause that other build step to be |
| // visited first. This is an error in the build file. |
| // For example, imagine: |
| // build generated.h: codegen_headers ... |
| // build generated.stamp: stamp || generated.h |
| // build foo.o: cc ... |
| // If we deps discover that foo.o depends on generated.h, |
| // we must have some dependency path from foo.o to generated.h, |
| // either direct or indirect (like the stamp). If that |
| // were present, then we'd already have file_state for this |
| // file and wouldn't get here. |
| anyhow::bail!( |
| "{}: used generated file {}, but has no dependency path to it", |
| build.location, |
| file.name |
| ); |
| } |
| self.file_state.stat(id, file.path())? |
| } |
| }; |
| if mtime == MTime::Missing { |
| return Ok(Some(id)); |
| } |
| } |
| Ok(None) |
| } |
| |
| /// Given a task that just finished, record any discovered deps and hash. |
| /// Postcondition: all outputs have been stat()ed. |
| fn record_finished(&mut self, id: BuildId, result: task::TaskResult) -> anyhow::Result<()> { |
| // Clean up the deps discovered from the task. |
| let mut deps = Vec::new(); |
| if let Some(names) = result.discovered_deps { |
| for name in names { |
| let fileid = self.graph.files.id_from_canonical(canon_path(name)); |
| // Filter duplicates from the file list. |
| if deps.contains(&fileid) { |
| continue; |
| } |
| // Filter out any deps that were already dirtying in the build file. |
| // Note that it's allowed to have a duplicate against an order-only |
| // dep; see `discover_existing_dep` test. |
| if self.graph.builds[id].dirtying_ins().contains(&fileid) { |
| continue; |
| } |
| deps.push(fileid); |
| } |
| } |
| |
| // We may have discovered new deps, so ensure we have mtimes for those. |
| let deps_changed = self.graph.builds[id].update_discovered(deps); |
| if deps_changed { |
| if let Some(missing) = self.ensure_input_files(id, true)? { |
| anyhow::bail!( |
| "{}: depfile references nonexistent {}", |
| self.graph.builds[id].location, |
| self.graph.file(missing).name |
| ); |
| } |
| } |
| |
| let input_was_missing = self.graph.builds[id] |
| .dirtying_ins() |
| .iter() |
| .any(|&id| self.file_state.get(id).unwrap() == MTime::Missing); |
| |
| // Update any cached state of the output files to reflect their new state. |
| let output_was_missing = self.stat_all_outputs(id)?.is_some(); |
| |
| if input_was_missing || output_was_missing { |
| // If a file is missing, don't record the build in in the db. |
| // It will be considered dirty next time anyway due to the missing file. |
| return Ok(()); |
| } |
| |
| let build = &self.graph.builds[id]; |
| let hash = hash::hash_build(&self.graph.files, &self.file_state, build); |
| self.db.write_build(&self.graph, id, hash)?; |
| |
| Ok(()) |
| } |
| |
| /// Given a build that just finished, check whether its dependent builds are now ready. |
| fn ready_dependents(&mut self, id: BuildId) { |
| let build = &self.graph.builds[id]; |
| self.build_states.set(id, build, BuildState::Done); |
| |
| let mut dependents = HashSet::new(); |
| for &id in build.outs() { |
| for &id in &self.graph.file(id).dependents { |
| if self.build_states.get(id) != BuildState::Want { |
| continue; |
| } |
| dependents.insert(id); |
| } |
| } |
| for id in dependents { |
| if !self.recheck_ready(id) { |
| continue; |
| } |
| self.build_states |
| .set(id, &self.graph.builds[id], BuildState::Ready); |
| } |
| } |
| |
| /// Stat all the outputs of a build. |
| /// Called before it's run (for determining whether it's up to date) and |
| /// after (to see if it touched any outputs). |
| fn stat_all_outputs(&mut self, id: BuildId) -> anyhow::Result<Option<FileId>> { |
| let build = &self.graph.builds[id]; |
| let mut missing = None; |
| for &id in build.outs() { |
| let file = self.graph.file(id); |
| let mtime = self.file_state.stat(id, file.path())?; |
| if mtime == MTime::Missing && missing.is_none() { |
| missing = Some(id); |
| } |
| } |
| Ok(missing) |
| } |
| |
| /// Stat all the input/output files for a given build in anticipation of |
| /// deciding whether it needs to be run again. |
| /// Prereq: any dependent input is already generated. |
| /// Returns a build error if any required input files are missing. |
| /// Otherwise returns the missing id if any expected but not required files, |
| /// e.g. outputs, are missing, implying that the build needs to be executed. |
| fn check_build_files_missing(&mut self, id: BuildId) -> anyhow::Result<Option<FileId>> { |
| // Ensure we have state for all input files. |
| if let Some(missing) = self.ensure_input_files(id, false)? { |
| let file = self.graph.file(missing); |
| if file.input.is_none() { |
| let build = &self.graph.builds[id]; |
| anyhow::bail!("{}: input {} missing", build.location, file.name); |
| } |
| return Ok(Some(missing)); |
| } |
| if let Some(missing) = self.ensure_input_files(id, true)? { |
| return Ok(Some(missing)); |
| } |
| |
| // Ensure we have state for all output files. |
| // We know this build is solely responsible for updating these outputs, |
| // and if we're checking if it's dirty we are visiting it the first |
| // time, so we stat unconditionally. |
| // This is looking at if the outputs are already present. |
| if let Some(missing) = self.stat_all_outputs(id)? { |
| return Ok(Some(missing)); |
| } |
| |
| // All files accounted for. |
| Ok(None) |
| } |
| |
| /// Like check_build_files_missing, but for phony rules, which have |
| /// different behavior for inputs. |
| fn check_build_files_missing_phony(&mut self, id: BuildId) -> anyhow::Result<()> { |
| // We don't consider the input files. This works around |
| // https://github.com/ninja-build/ninja/issues/1779 |
| // which is a bug that a phony rule with a missing input |
| // dependency doesn't fail the build. |
| // TODO: key this behavior off of the "ninja compat" flag. |
| // TODO: reconsider how phony deps work, maybe we should always promote |
| // phony deps to order-only? |
| |
| // Maintain the invariant that we have stat info for all outputs, but |
| // we generally don't expect them to have been created. |
| // TODO: what should happen if a rule uses a phony output as its own input? |
| // The Ninja manual suggests you can use phony rules to aggregate outputs |
| // together, so we might need to create some sort of fake mtime here? |
| self.stat_all_outputs(id)?; |
| Ok(()) |
| } |
| |
| /// Check a ready build for whether it needs to run, returning true if so. |
| /// Prereq: any dependent input is already generated. |
| fn check_build_dirty(&mut self, id: BuildId) -> anyhow::Result<bool> { |
| let build = &self.graph.builds[id]; |
| let phony = build.cmdline.is_none(); |
| let file_missing = if phony { |
| self.check_build_files_missing_phony(id)?; |
| return Ok(false); // Phony builds never need to run anything. |
| } else { |
| self.check_build_files_missing(id)? |
| }; |
| |
| // If any files are missing, the build is dirty without needing |
| // to consider hashes. |
| let build = &self.graph.builds[id]; |
| if let Some(missing) = file_missing { |
| if self.options.explain { |
| self.progress.log(&format!( |
| "explain: {}: input {} missing", |
| build.location, |
| self.graph.file(missing).name |
| )); |
| } |
| return Ok(true); |
| } |
| |
| // If we get here, all the relevant files are present and stat()ed, |
| // so compare the hash against the last hash. |
| |
| // TODO: skip this whole function if no previous hash is present. |
| // More complex than just moving this block up, because we currently |
| // assume that we've always checked inputs after we've run a build. |
| let prev_hash = match self.last_hashes.get(id) { |
| None => { |
| if self.options.explain { |
| self.progress.log(&format!( |
| "explain: {}: no previous state known", |
| build.location |
| )); |
| } |
| return Ok(true); |
| } |
| Some(prev_hash) => prev_hash, |
| }; |
| |
| let hash = hash::hash_build(&self.graph.files, &self.file_state, build); |
| if prev_hash != hash { |
| if self.options.explain { |
| self.progress |
| .log(&format!("explain: {}: manifest changed", build.location)); |
| self.progress.log(&hash::explain_hash_build( |
| &self.graph.files, |
| &self.file_state, |
| build, |
| )); |
| } |
| return Ok(true); |
| } |
| |
| Ok(false) |
| } |
| |
| /// Create the parent directories of a given list of fileids. |
| /// Used to create directories used for outputs. |
| /// TODO: do this within the thread executing the subtask? |
| fn create_parent_dirs(&self, ids: &[FileId]) -> anyhow::Result<()> { |
| let mut dirs: Vec<&std::path::Path> = Vec::new(); |
| for &out in ids { |
| if let Some(parent) = self.graph.file(out).path().parent() { |
| if dirs.iter().any(|&p| p == parent) { |
| continue; |
| } |
| std::fs::create_dir_all(parent)?; |
| dirs.push(parent); |
| } |
| } |
| Ok(()) |
| } |
| |
| /// Runs the build. |
| /// Returns the number of tasks executed on successful builds, or None on failed builds. |
| pub fn run(&mut self) -> anyhow::Result<Option<usize>> { |
| #[cfg(unix)] |
| signal::register_sigint(); |
| let mut tasks_done = 0; |
| let mut tasks_failed = 0; |
| let mut runner = task::Runner::new(self.options.parallelism); |
| while self.build_states.unfinished() { |
| self.progress.update(&self.build_states.counts); |
| |
| // Approach: |
| // - First make sure we're running as many queued tasks as the runner |
| // allows. |
| // - Next make sure we've finished or enqueued any tasks that are |
| // ready. |
| // - If either one of those made progress, loop, to ensure the other |
| // one gets to work from the result. |
| // - If neither made progress, wait for a task to complete and |
| // loop. |
| |
| let mut made_progress = false; |
| while runner.can_start_more() { |
| let id = match self.build_states.pop_queued() { |
| Some(id) => id, |
| None => break, |
| }; |
| let build = &self.graph.builds[id]; |
| self.build_states.set(id, build, BuildState::Running); |
| self.create_parent_dirs(build.outs())?; |
| runner.start(id, build); |
| self.progress.task_started(id, build); |
| made_progress = true; |
| } |
| |
| while let Some(id) = self.build_states.pop_ready() { |
| if !self.check_build_dirty(id)? { |
| // Not dirty; go directly to the Done state. |
| self.ready_dependents(id); |
| } else if self.options.adopt { |
| // Act as if the target already finished. |
| self.record_finished( |
| id, |
| task::TaskResult { |
| termination: process::Termination::Success, |
| output: vec![], |
| discovered_deps: None, |
| }, |
| )?; |
| self.ready_dependents(id); |
| } else { |
| self.build_states.enqueue(id, &self.graph.builds[id])?; |
| } |
| made_progress = true; |
| } |
| |
| if made_progress { |
| continue; |
| } |
| |
| if !runner.is_running() { |
| if tasks_failed > 0 { |
| // No more progress can be made, hopefully due to tasks that failed. |
| break; |
| } |
| panic!("BUG: no work to do and runner not running"); |
| } |
| |
| let task = runner.wait(|id, line| { |
| self.progress.task_output(id, line); |
| }); |
| let build = &self.graph.builds[task.buildid]; |
| trace::if_enabled(|t| { |
| let desc = progress::build_message(build); |
| t.write_complete(desc, task.tid + 1, task.span.0, task.span.1); |
| }); |
| |
| self.progress |
| .task_finished(task.buildid, build, &task.result); |
| match task.result.termination { |
| process::Termination::Failure => { |
| if let Some(failures_left) = &mut self.options.failures_left { |
| *failures_left -= 1; |
| if *failures_left == 0 { |
| return Ok(None); |
| } |
| } |
| tasks_failed += 1; |
| self.build_states |
| .set(task.buildid, build, BuildState::Failed); |
| } |
| process::Termination::Interrupted => { |
| // If the task was interrupted bail immediately. |
| return Ok(None); |
| } |
| process::Termination::Success => { |
| tasks_done += 1; |
| self.record_finished(task.buildid, task.result)?; |
| self.ready_dependents(task.buildid); |
| } |
| }; |
| } |
| |
| // If the user ctl-c's, it likely caused a subtask to fail. |
| // But at least for the LLVM test suite it can catch sigint and print |
| // "interrupted by user" and exit with success, and in that case we |
| // don't want n2 to print a "succeeded" message afterwards. |
| let success = tasks_failed == 0 && !signal::was_interrupted(); |
| Ok(success.then_some(tasks_done)) |
| } |
| } |
| |
| #[cfg(test)] |
| mod tests { |
| use super::*; |
| |
| #[test] |
| fn build_cycle() -> Result<(), anyhow::Error> { |
| let file = " |
| build a: phony b |
| build b: phony c |
| build c: phony a |
| "; |
| let mut graph = crate::load::parse("build.ninja", file.as_bytes().to_vec())?; |
| let a_id = graph.files.id_from_canonical("a"); |
| let mut states = BuildStates::new(graph.builds.next_id(), SmallMap::default()); |
| let mut stack = Vec::new(); |
| match states.want_file(&graph, &mut stack, a_id) { |
| Ok(_) => panic!("expected build cycle error"), |
| Err(err) => assert_eq!(err.to_string(), "dependency cycle: a -> b -> c -> a"), |
| } |
| Ok(()) |
| } |
| } |