blob: eaf4d007062c6ea0c827a6f75441ce71bd33a92c [file] [log] [blame]
//! Graph loading: runs .ninja parsing and constructs the build graph from it.
use crate::{
canon::canon_path,
db,
file_pool::FilePool,
graph::{self, BuildId, Graph, GraphFiles},
parse::{self, Clump, ClumpOrInclude, Rule, VariableAssignment},
scanner::{format_parse_error, ParseResult},
smallmap::SmallMap,
trace,
};
use anyhow::{anyhow, bail};
use log::debug;
use rayon::prelude::*;
use rustc_hash::FxHashMap;
use std::path::{Path, PathBuf};
use std::{collections::hash_map::Entry, sync::Arc, thread::available_parallelism};
/// ScopePosition represents the position of a statement (usually variable
/// assignments, but also important for rules and subninjas) within the scope.
/// This is not the same as the position within a file, because included files
/// are part of the same scope, and thus the ScopePositions must be continuous
/// across them. ScopePositions are useful for evaluating variable assignments
/// in parallel, because we can use them to know which assignments to the same
/// variable come before the current assignment, and must be taken into account.
#[derive(Debug, Copy, Clone, PartialEq, Eq, Default, PartialOrd, Ord)]
pub struct ScopePosition(pub usize);
impl ScopePosition {
/// Adds the two scope positions together and returns a new ScopePostition
pub fn add(&self, other: ScopePosition) -> ScopePosition {
ScopePosition(self.0 + other.0)
}
/// Adds an integer value to the given ScopePosition and returns a new
/// ScopePosition.
pub fn add_usize(&self, other: usize) -> ScopePosition {
ScopePosition(self.0 + other)
}
}
/// ParentScopeReference is a reference to the parent scope + the ScopePosition
/// of the subninja statement that's creating the sub-scope.
#[derive(Debug)]
pub struct ParentScopeReference(pub Arc<Scope>, pub ScopePosition);
/// A Scope represents a collection of rules and variable assignments within
/// the set of ninja files within a subninja statement. Subninja statements
/// create a new scope for the files they load, and the sub-scope inherits
/// rules and variables from its parent scope.
#[derive(Debug)]
pub struct Scope {
parent: Option<ParentScopeReference>,
rules: FxHashMap<String, Rule>,
variables: FxHashMap<String, Vec<VariableAssignment>>,
}
impl Scope {
/// Creates a new scope with an optional parent scope
pub fn new(parent: Option<ParentScopeReference>) -> Self {
Self {
parent,
rules: FxHashMap::default(),
variables: FxHashMap::default(),
}
}
/// Gets the rule with the given name at the given ScopePostion. A single
/// scope can't define the rule more than once, but a parent scope and
/// the current scope may define it, in which case the parent scope's will
/// be returned if the position is before the current scope's rule.
pub fn get_rule(&self, name: &str, position: ScopePosition) -> Option<&Rule> {
match self.rules.get(name) {
Some(rule) if rule.scope_position.0 < position.0 => Some(rule),
Some(_) | None => self
.parent
.as_ref()
.map(|p| p.0.get_rule(name, p.1))
.flatten(),
}
}
/// Evaluates the given variable at the given ScopePosition, and appends
/// the result onto the result String.
pub fn evaluate(&self, result: &mut String, varname: &str, position: ScopePosition) {
if let Some(variables) = self.variables.get(varname) {
let i = variables.binary_search_by_key(&position, |x| x.scope_position);
let i = match i {
Ok(i) => std::cmp::max(i, 1) - 1,
Err(i) => std::cmp::min(i, variables.len() - 1),
};
if variables[i].scope_position.0 < position.0 {
variables[i].evaluate(result, &self);
return;
}
// We couldn't find a variable assignment before the input
// position, so check the parent scope if there is one.
}
if let Some(parent) = &self.parent {
parent.0.evaluate(result, varname, parent.1);
}
}
}
fn evaluate_build_files<'text>(
files: &GraphFiles,
scope: Arc<Scope>,
b: &mut graph::Build,
base_position: ScopePosition,
) -> anyhow::Result<()> {
b.scope_position.0 += base_position.0;
let num_outs = b.outs.num_outs();
b.outs.ids = b.unevaluated_outs_and_ins[..num_outs]
.iter()
.map(|x| {
files.id_from_canonical(canon_path(x.evaluate(
&[&b.bindings],
&scope,
b.scope_position,
)))
})
.collect();
b.ins.ids = b.unevaluated_outs_and_ins[num_outs..]
.iter()
.map(|x| {
files.id_from_canonical(canon_path(x.evaluate(
&[&b.bindings],
&scope,
b.scope_position,
)))
})
.collect();
// The unevaluated values actually have a lifetime of 'text, not 'static,
// so clear them so they don't accidentally get used later.
b.unevaluated_outs_and_ins.clear();
b.unevaluated_outs_and_ins.shrink_to_fit();
b.scope = Some(scope);
Ok(())
}
#[derive(Default)]
struct SubninjaResults<'text> {
clumps: Vec<Clump<'text>>,
builddir: Option<String>,
}
fn subninja<'thread, 'text>(
num_threads: usize,
files: &'thread GraphFiles,
file_pool: &'text FilePool,
path: String,
parent_scope: Option<ParentScopeReference>,
) -> anyhow::Result<SubninjaResults<'text>>
where
'text: 'thread,
{
debug!("Started loading subninja: {}", path);
let path = PathBuf::from(path);
let top_level_scope = parent_scope.is_none();
let mut scope = Scope::new(parent_scope);
if top_level_scope {
scope.rules.insert(
"phony".to_owned(),
Rule {
vars: SmallMap::default(),
scope_position: ScopePosition(0),
},
);
}
let filename = Arc::new(path);
let mut parse_results = trace::scope("parse", || {
parse(
&filename,
num_threads,
file_pool,
file_pool.read_file(&filename)?,
&mut scope,
// to account for the phony rule
if top_level_scope {
ScopePosition(1)
} else {
ScopePosition(0)
},
)
})?;
let scope = Arc::new(scope);
for clump in &mut parse_results {
let base_position = clump.base_position;
for default in clump.defaults.iter_mut() {
let scope = scope.clone();
default.evaluated = default
.files
.iter()
.map(|x| {
let path = canon_path(x.evaluate(
&[],
&scope,
default.scope_position.add(base_position),
));
files.id_from_canonical(path)
})
.collect();
}
}
trace::scope("evaluate builds' files", || -> anyhow::Result<()> {
parse_results
.par_iter_mut()
.flat_map(|x| {
let num_builds = x.builds.len();
x.builds
.par_iter_mut()
.zip(rayon::iter::repeatn(x.base_position, num_builds))
})
.try_for_each(|(mut build, base_position)| -> anyhow::Result<()> {
evaluate_build_files(files, scope.clone(), &mut build, base_position)
})
})?;
// The unevaluated values of scoped variables have a lifetime of 'static
// for simplicity in the code, but in actuality their lifetime is 'text.
// We need to evaluate all the variables before the lifetime of 'text ends.
scope
.variables
.par_iter()
.flat_map(|x| x.1.par_iter())
.for_each(|x| {
x.pre_evaluate(&scope);
});
let mut subninja_results = parse_results
.par_iter()
.flat_map(|x| {
x.subninjas
.par_iter()
.zip(rayon::iter::repeatn(x.base_position, x.subninjas.len()))
})
.map(|(sn, base_position)| -> anyhow::Result<Vec<Clump>> {
let position = sn.scope_position.add(base_position);
let file = canon_path(sn.file.evaluate(&[], &scope, position));
Ok(subninja(
num_threads,
files,
file_pool,
file,
Some(ParentScopeReference(scope.clone(), position)),
)?
.clumps)
})
.collect::<anyhow::Result<Vec<Vec<Clump<'text>>>>>()?;
for subninja_result in &mut subninja_results {
parse_results.append(subninja_result);
}
// Only the builddir in the outermost scope is respected
let build_dir = if top_level_scope {
let mut build_dir = String::new();
scope.evaluate(&mut build_dir, "builddir", ScopePosition(usize::MAX));
if !build_dir.is_empty() {
Some(build_dir)
} else {
None
}
} else {
None
};
Ok(SubninjaResults {
clumps: parse_results,
builddir: build_dir,
})
}
fn include<'thread, 'text>(
filename: &Arc<PathBuf>,
num_threads: usize,
file_pool: &'text FilePool,
path: String,
scope: &mut Scope,
clump_base_position: ScopePosition,
) -> anyhow::Result<Vec<parse::Clump<'text>>>
where
'text: 'thread,
{
let path = PathBuf::from(path);
parse(
filename,
num_threads,
file_pool,
file_pool.read_file(&path)?,
scope,
clump_base_position,
)
}
fn parse<'thread, 'text>(
filename: &Arc<PathBuf>,
num_threads: usize,
file_pool: &'text FilePool,
bytes: &'text [u8],
scope: &mut Scope,
mut clump_base_position: ScopePosition,
) -> anyhow::Result<Vec<parse::Clump<'text>>>
where
'text: 'thread,
{
let chunks = parse::split_manifest_into_chunks(bytes, num_threads);
let statements: ParseResult<Vec<Vec<ClumpOrInclude>>> = chunks
.par_iter()
.enumerate()
.map(|(i, chunk)| {
let mut parser = parse::Parser::new(chunk, filename.clone(), i);
parser.read_clumps()
})
.collect();
let Ok(statements) = statements else {
let err = statements.unwrap_err();
let ofs = chunks[..err.get_chunk_index()].iter().map(|x| x.len()).sum();
bail!(format_parse_error(
ofs,
chunks[err.get_chunk_index()],
filename,
err
));
};
let mut num_rules = 0;
let mut num_variables = 0;
let mut num_clumps = 0;
for clumps in &statements {
num_clumps += clumps.len();
for clump_or_include in clumps {
if let ClumpOrInclude::Clump(clump) = clump_or_include {
num_rules += clump.rules.len();
num_variables += clump.assignments.len();
}
}
}
scope.rules.reserve(num_rules);
scope.variables.reserve(num_variables);
let mut results = Vec::with_capacity(num_clumps);
for stmt in statements.into_iter().flatten() {
match stmt {
ClumpOrInclude::Clump(mut clump) => {
// Variable assignments must be added to the scope now, because
// they may be referenced by a later include. Also add rules
// while we're at it, to avoid some copies later on.
let rules = std::mem::take(&mut clump.rules);
let assignments = std::mem::take(&mut clump.assignments);
let scope_rules = &mut scope.rules;
let scope_variables = &mut scope.variables;
rayon::join(
|| {
for (name, mut variable_assignment) in assignments.into_iter() {
variable_assignment.scope_position.0 += clump_base_position.0;
match scope_variables.entry(name) {
Entry::Occupied(mut e) => e.get_mut().push(variable_assignment),
Entry::Vacant(e) => {
e.insert(vec![variable_assignment]);
}
}
}
},
|| -> anyhow::Result<()> {
for (name, mut rule) in rules.into_iter() {
rule.scope_position.0 += clump_base_position.0;
match scope_rules.entry(name) {
Entry::Occupied(e) => bail!("duplicate rule '{}'", e.key()),
Entry::Vacant(e) => {
e.insert(rule);
}
}
}
Ok(())
},
)
.1?;
clump.base_position = clump_base_position;
clump_base_position.0 += clump.used_scope_positions;
results.push(clump);
}
ClumpOrInclude::Include(i) => {
trace::scope("include", || -> anyhow::Result<()> {
let evaluated = canon_path(i.evaluate(&[], &scope, clump_base_position));
let mut new_results = include(
filename,
num_threads,
file_pool,
evaluated,
scope,
clump_base_position,
)?;
clump_base_position = new_results
.last()
.map(|c| c.base_position.add_usize(c.used_scope_positions))
.unwrap_or(clump_base_position);
results.append(&mut new_results);
Ok(())
})?;
}
}
}
Ok(results)
}
/// State loaded by read().
pub struct State {
/// The graph of builds and files
pub graph: graph::Graph,
/// The database used for persisting information between runs of n2
pub db: db::Writer,
/// The hashes of the builds from the previous run, loaded from the database
pub hashes: graph::Hashes,
/// The default files to build
pub default: Vec<Arc<graph::File>>,
/// The pools defined in the ninja file
pub pools: SmallMap<String, usize>,
}
/// Load build.ninja/.n2_db and return the loaded build graph and state.
pub fn read(build_filename: &str) -> anyhow::Result<State> {
let build_filename = canon_path(build_filename);
debug!("Started loading {}", build_filename);
let file_pool = FilePool::new();
let num_threads = available_parallelism()?.get();
let pool = rayon::ThreadPoolBuilder::new()
.num_threads(num_threads)
.build()?;
trace::scope("loader.read_file", || -> anyhow::Result<_> {
pool.scope(|_| {
let files = GraphFiles::default();
let mut results = subninja(num_threads, &files, &file_pool, build_filename, None)?;
let mut pools = SmallMap::default();
let mut defaults = Vec::new();
let mut num_builds = 0;
trace::scope("add pools and defaults", || -> anyhow::Result<()> {
for clump in &mut results.clumps {
for pool in &clump.pools {
if !pools.insert_if_absent(pool.name.to_owned(), pool.depth) {
bail!("duplicate pool {}", pool.name);
}
}
for default in &mut clump.defaults {
defaults.append(&mut default.evaluated);
}
num_builds += clump.builds.len();
}
Ok(())
})?;
let mut builds = trace::scope("allocate and concat builds", || {
let mut builds = Vec::with_capacity(num_builds);
for clump in &mut results.clumps {
builds.append(&mut clump.builds);
}
builds
});
let builddir = results.builddir.take();
drop(results);
// Turns out munmap is rather slow, unmapping the android ninja
// files takes ~150ms. Do this in parallel with initialize_build.
rayon::spawn(move || {
drop(file_pool);
});
trace::scope("initialize builds", || {
builds
.par_iter_mut()
.enumerate()
.try_for_each(|(id, build)| {
build.id = BuildId::from(id);
graph::Graph::initialize_build(build)
})
})?;
let mut graph = Graph::new(builds, files)?;
let mut hashes = graph::Hashes::default();
let db = trace::scope("db::open", || {
let mut db_path = PathBuf::from(".n2_db");
if let Some(builddir) = &builddir {
db_path = Path::new(&builddir).join(db_path);
if let Some(parent) = db_path.parent() {
std::fs::create_dir_all(parent)?;
}
};
db::open(&db_path, &mut graph, &mut hashes)
})
.map_err(|err| anyhow!("load .n2_db: {}", err))?;
debug!("successfully loaded ninja file");
Ok(State {
graph,
db,
hashes,
default: defaults,
pools,
})
})
})
}