Anki/build/ninja_gen/src/build.rs
Damien Elmes 8e13e6bfc1 Update n2 [action required]
Make sure to run tools/install-n2 after updating to this commit.
n2 have merged in some changes we were previously hosting in a fork,
but the parsing of the flags was altered.
2025-02-19 10:34:45 +07:00

566 lines
19 KiB
Rust

// Copyright: Ankitects Pty Ltd and contributors
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
use std::collections::HashMap;
use std::collections::HashSet;
use std::fmt::Write;
use anyhow::Result;
use camino::Utf8PathBuf;
use itertools::Itertools;
use crate::action::BuildAction;
use crate::archives::Platform;
use crate::configure::ConfigureBuild;
use crate::input::space_separated;
use crate::input::BuildInput;
#[derive(Debug)]
pub struct Build {
pub variables: HashMap<&'static str, String>,
pub buildroot: Utf8PathBuf,
pub build_profile: BuildProfile,
pub pools: Vec<(&'static str, usize)>,
pub trailing_text: String,
pub host_platform: Platform,
pub have_n2: bool,
pub(crate) output_text: String,
action_names: HashSet<&'static str>,
pub(crate) groups: HashMap<String, Vec<String>>,
}
impl Build {
pub fn new() -> Result<Self> {
let buildroot = if cfg!(windows) {
Utf8PathBuf::from("out")
} else {
// on Unix systems we allow out to be a symlink to an external location
Utf8PathBuf::from("out").canonicalize_utf8()?
};
let mut build = Build {
buildroot,
build_profile: BuildProfile::from_env(),
host_platform: Platform::current(),
variables: Default::default(),
pools: Default::default(),
trailing_text: Default::default(),
output_text: Default::default(),
action_names: Default::default(),
groups: Default::default(),
have_n2: which::which("n2").is_ok(),
};
build.add_action("build:configure", ConfigureBuild {})?;
Ok(build)
}
pub fn variable(&mut self, name: &'static str, value: impl Into<String>) {
self.variables.insert(name, value.into());
}
pub fn pool(&mut self, name: &'static str, size: usize) {
self.pools.push((name, size));
}
/// Evaluate the provided closure only once, using `key` to determine
/// uniqueness. This key should not match any build action name.
pub fn once_only(
&mut self,
key: &'static str,
block: impl FnOnce(&mut Build) -> Result<()>,
) -> Result<()> {
if self.action_names.insert(key) {
block(self)
} else {
Ok(())
}
}
pub fn add_action(&mut self, group: impl AsRef<str>, action: impl BuildAction) -> Result<()> {
let group = group.as_ref();
let groups = split_groups(group);
let group = groups[0];
let command = action.command();
let action_name = action.name();
// first invocation?
let mut first_invocation = false;
self.once_only(action_name, |build| {
action.on_first_instance(build)?;
first_invocation = true;
Ok(())
})?;
let action_name = action_name.to_string();
// ensure separator is delivered to runner, not shell
let command = if cfg!(windows) || action.bypass_runner() {
command.into()
} else {
command.replace("&&", "\"&&\"")
};
let mut statement = BuildStatement::from_build_action(
group,
action,
&self.groups,
self.build_profile,
self.have_n2,
);
if first_invocation {
let command = statement.prepare_command(command)?;
writeln!(
&mut self.output_text,
"\
rule {action_name}
command = {command}",
)
.unwrap();
for (k, v) in &statement.rule_variables {
writeln!(&mut self.output_text, " {k} = {v}").unwrap();
}
self.output_text.push('\n');
}
let (all_outputs, subgroups) = statement.render_into(&mut self.output_text);
for group in groups {
self.add_resolved_files_to_group(group, &all_outputs);
}
for (subgroup, outputs) in subgroups {
let group_with_subgroup = format!("{group}:{subgroup}");
self.add_resolved_files_to_group(&group_with_subgroup, &outputs);
}
Ok(())
}
/// Add one or more resolved files to a group. Does not add to the parent
/// groups; that must be done by the caller.
fn add_resolved_files_to_group<'a>(
&mut self,
group: &str,
files: impl IntoIterator<Item = &'a String>,
) {
let buf = self.groups.entry(group.to_owned()).or_default();
buf.extend(files.into_iter().map(ToString::to_string));
}
/// Allows you to add dependencies on files or build steps that aren't
/// required to build the group itself, but are required by consumers of
/// that group. Can also be used to allow substitution of local binaries
/// for downloaded ones (eg :node_binary).
pub fn add_dependency(&mut self, group: &str, deps: BuildInput) {
let files = self.expand_inputs(deps);
let groups = split_groups(group);
for group in groups {
self.add_resolved_files_to_group(group, &files);
}
}
/// Outputs from a given build statement group. An error if no files have
/// been registered yet.
pub fn group_outputs(&self, group_name: &'static str) -> &[String] {
self.groups
.get(group_name)
.unwrap_or_else(|| panic!("expected files in {group_name}"))
}
/// Single output from a given build statement group. An error if no files
/// have been registered yet, or more than one file has been registered.
pub fn group_output(&self, group_name: &'static str) -> String {
let outputs = self.group_outputs(group_name);
assert_eq!(outputs.len(), 1);
outputs.first().unwrap().into()
}
pub fn expand_inputs(&self, inputs: impl AsRef<BuildInput>) -> Vec<String> {
expand_inputs(inputs, &self.groups)
}
/// Expand inputs, the return a filtered subset.
pub fn filter_inputs<F>(&self, inputs: impl AsRef<BuildInput>, func: F) -> Vec<String>
where
F: FnMut(&String) -> bool,
{
self.expand_inputs(inputs)
.into_iter()
.filter(func)
.collect()
}
pub fn inputs_with_suffix(&self, inputs: impl AsRef<BuildInput>, ext: &str) -> Vec<String> {
self.filter_inputs(inputs, |f| f.ends_with(ext))
}
}
fn split_groups(group: &str) -> Vec<&str> {
let mut rest = group;
let mut groups = vec![group];
while let Some((head, _tail)) = rest.rsplit_once(':') {
groups.push(head);
rest = head;
}
groups
}
struct BuildStatement<'a> {
/// Cache of outputs by already-evaluated build rules, allowing later rules
/// to more easily consume the outputs of previous rules.
existing_outputs: &'a HashMap<String, Vec<String>>,
rule_name: &'static str,
// implicit refers to files that are not automatically assigned to $in and $out by Ninja,
implicit_inputs: Vec<String>,
implicit_outputs: Vec<String>,
explicit_inputs: Vec<String>,
explicit_outputs: Vec<String>,
order_only_inputs: Vec<String>,
output_subsets: Vec<(String, Vec<String>)>,
variables: Vec<(String, String)>,
rule_variables: Vec<(String, String)>,
output_stamp: bool,
env_vars: Vec<String>,
working_dir: Option<String>,
create_dirs: Vec<String>,
build_profile: BuildProfile,
bypass_runner: bool,
}
impl BuildStatement<'_> {
fn from_build_action<'a>(
group: &str,
mut action: impl BuildAction,
existing_outputs: &'a HashMap<String, Vec<String>>,
build_profile: BuildProfile,
have_n2: bool,
) -> BuildStatement<'a> {
let mut stmt = BuildStatement {
existing_outputs,
rule_name: action.name(),
implicit_inputs: Default::default(),
implicit_outputs: Default::default(),
explicit_inputs: Default::default(),
explicit_outputs: Default::default(),
order_only_inputs: Default::default(),
variables: Default::default(),
rule_variables: Default::default(),
output_subsets: Default::default(),
output_stamp: false,
env_vars: Default::default(),
working_dir: None,
create_dirs: Default::default(),
build_profile,
bypass_runner: action.bypass_runner(),
};
action.files(&mut stmt);
if stmt.explicit_outputs.is_empty() && stmt.implicit_outputs.is_empty() {
panic!("{} must generate at least one output", action.name());
}
stmt.variables.push(("description".into(), group.into()));
if action.check_output_timestamps() {
stmt.rule_variables.push(("restat".into(), "1".into()));
}
if action.generator() {
stmt.rule_variables.push(("generator".into(), "1".into()));
}
if let Some(pool) = action.concurrency_pool() {
stmt.rule_variables.push(("pool".into(), pool.into()));
}
if have_n2 {
if action.hide_success() {
stmt.rule_variables
.push(("hide_success".into(), "1".into()));
}
if action.hide_progress() {
stmt.rule_variables
.push(("hide_progress".into(), "1".into()));
}
}
stmt
}
/// Returns a list of all output files, which `Build` will add to
/// `existing_outputs`, and any subgroups.
fn render_into(mut self, buf: &mut String) -> (Vec<String>, Vec<(String, Vec<String>)>) {
let action_name = self.rule_name;
self.implicit_inputs.sort();
self.implicit_outputs.sort();
let inputs_str = to_ninja_target_string(
&self.explicit_inputs,
&self.implicit_inputs,
&self.order_only_inputs,
);
let outputs_str =
to_ninja_target_string(&self.explicit_outputs, &self.implicit_outputs, &[]);
writeln!(buf, "build {outputs_str}: {action_name} {inputs_str}").unwrap();
for (key, value) in self.variables.iter().sorted() {
writeln!(buf, " {key} = {}", value).unwrap();
}
writeln!(buf).unwrap();
let outputs_vec = {
self.implicit_outputs.extend(self.explicit_outputs);
self.implicit_outputs
};
(outputs_vec, self.output_subsets)
}
fn prepare_command(&mut self, command: String) -> Result<String> {
if self.bypass_runner {
return Ok(command);
}
if command.starts_with("$runner") {
self.implicit_inputs.push("$runner".into());
return Ok(command);
}
let mut buf = String::from("$runner run ");
if self.output_stamp {
write!(&mut buf, "--stamp=$stamp ")?;
}
for var in &self.env_vars {
write!(&mut buf, "--env=\"{var}\" ")?;
}
for dir in &self.create_dirs {
write!(&mut buf, "--mkdir={dir} ")?;
}
if let Some(working_dir) = &self.working_dir {
write!(&mut buf, "--cwd={working_dir} ")?;
}
buf.push_str(&command);
Ok(buf)
}
}
fn expand_inputs(
input: impl AsRef<BuildInput>,
existing_outputs: &HashMap<String, Vec<String>>,
) -> Vec<String> {
let mut vec = vec![];
input.as_ref().add_to_vec(&mut vec, existing_outputs);
vec
}
#[derive(Debug, Eq, PartialEq, Clone, Copy)]
pub enum BuildProfile {
Debug,
Release,
ReleaseWithLto,
}
impl BuildProfile {
fn from_env() -> Self {
match std::env::var("RELEASE").unwrap_or_default().as_str() {
"1" => Self::Release,
"2" => Self::ReleaseWithLto,
_ => Self::Debug,
}
}
}
pub trait FilesHandle {
/// Add inputs to the build statement. Can be called multiple times with
/// different variables. This is a shortcut for calling .expand_inputs()
/// and then .add_inputs_vec()
/// - If the variable name is non-empty, a variable of the same name will be
/// created so the file list can be accessed in the command. By
/// convention, this is often `in`.
fn add_inputs(&mut self, variable: &'static str, inputs: impl AsRef<BuildInput>);
fn add_inputs_vec(&mut self, variable: &'static str, inputs: Vec<String>);
fn add_order_only_inputs(&mut self, variable: &'static str, inputs: impl AsRef<BuildInput>);
/// Add a variable that can be referenced in the command.
fn add_variable(&mut self, name: impl Into<String>, value: impl Into<String>);
fn expand_input(&self, input: &BuildInput) -> String;
fn expand_inputs(&self, inputs: impl AsRef<BuildInput>) -> Vec<String>;
/// Like [FilesHandle::add_outputs_ext], without adding a subgroup.
fn add_outputs(
&mut self,
variable: &'static str,
outputs: impl IntoIterator<Item = impl AsRef<str>>,
) {
self.add_outputs_ext(variable, outputs, false);
}
/// Add outputs to the build statement. Can be called multiple times with
/// different variables.
/// - Each output automatically has $builddir/ prefixed to it if it does not
/// already start with it.
/// - If the variable name is non-empty, a variable of the same name will be
/// created so the file list can be accessed in the command. By
/// convention, this is often `out`.
/// - If subgroup is true, the files are also placed in a subgroup. Eg if a
/// rule `foo` exists and subgroup `bar` is provided, the files are
/// accessible via `:foo:bar`. The variable name must not be empty, or
/// called `out`.
fn add_outputs_ext(
&mut self,
variable: impl Into<String>,
outputs: impl IntoIterator<Item = impl AsRef<str>>,
subgroup: bool,
);
/// Save an output stamp if the command completes successfully. Note that
/// if you have bypassed the runner, you will need to create the file
/// yourself.
fn add_output_stamp(&mut self, path: impl Into<String>);
/// Set an env var for the duration of the provided command(s).
/// Note this is defined once for the rule, so if the value should change
/// for each command, `constant_value` should reference a `$variable` you
/// have defined.
fn add_env_var(&mut self, key: &str, constant_value: &str);
/// Set the current working dir for the provided command(s).
/// Note this is defined once for the rule, so if the value should change
/// for each command, `constant_value` should reference a `$variable` you
/// have defined.
fn set_working_dir(&mut self, constant_value: &str);
/// Ensure provided folder and parent folders are created before running
/// the command. Can be called multiple times. Defines a variable pointing
/// at the folder.
fn create_dir_all(&mut self, key: &str, path: impl Into<String>);
fn build_profile(&self) -> BuildProfile;
}
impl FilesHandle for BuildStatement<'_> {
fn add_inputs(&mut self, variable: &'static str, inputs: impl AsRef<BuildInput>) {
self.add_inputs_vec(variable, FilesHandle::expand_inputs(self, inputs));
}
fn add_inputs_vec(&mut self, variable: &'static str, inputs: Vec<String>) {
match variable {
"in" => self.explicit_inputs.extend(inputs),
other_key => {
if !other_key.is_empty() {
self.add_variable(other_key, space_separated(&inputs));
}
self.implicit_inputs.extend(inputs);
}
}
}
fn add_order_only_inputs(&mut self, variable: &'static str, inputs: impl AsRef<BuildInput>) {
let inputs = FilesHandle::expand_inputs(self, inputs);
if !variable.is_empty() {
self.add_variable(variable, space_separated(&inputs))
}
self.order_only_inputs.extend(inputs);
}
fn add_variable(&mut self, key: impl Into<String>, value: impl Into<String>) {
self.variables.push((key.into(), value.into()));
}
fn expand_input(&self, input: &BuildInput) -> String {
let mut vec = Vec::with_capacity(1);
input.add_to_vec(&mut vec, self.existing_outputs);
if vec.len() != 1 {
panic!("expected {input:?} to resolve to a single file; got ${vec:?}");
}
vec.pop().unwrap()
}
fn add_outputs_ext(
&mut self,
variable: impl Into<String>,
outputs: impl IntoIterator<Item = impl AsRef<str>>,
subgroup: bool,
) {
let outputs = outputs.into_iter().map(|v| {
let v = v.as_ref();
let v = if !v.starts_with("$builddir/") && !v.starts_with("$builddir\\") {
format!("$builddir/{}", v)
} else {
v.to_owned()
};
if cfg!(windows) {
v.replace('/', "\\")
} else {
v
}
});
let variable = variable.into();
match variable.as_str() {
"out" => self.explicit_outputs.extend(outputs),
other_key => {
let outputs: Vec<_> = outputs.collect();
if !other_key.is_empty() {
self.add_variable(other_key, space_separated(&outputs));
}
if subgroup {
assert!(!other_key.is_empty());
self.output_subsets
.push((other_key.to_owned(), outputs.to_owned()));
}
self.implicit_outputs.extend(outputs);
}
}
}
fn expand_inputs(&self, inputs: impl AsRef<BuildInput>) -> Vec<String> {
expand_inputs(inputs, self.existing_outputs)
}
fn build_profile(&self) -> BuildProfile {
self.build_profile
}
fn add_output_stamp(&mut self, path: impl Into<String>) {
self.output_stamp = true;
self.add_outputs("stamp", vec![path.into()]);
}
fn add_env_var(&mut self, key: &str, constant_value: &str) {
self.env_vars.push(format!("{key}={constant_value}"));
}
fn set_working_dir(&mut self, constant_value: &str) {
self.working_dir = Some(constant_value.to_owned());
}
fn create_dir_all(&mut self, key: &str, path: impl Into<String>) {
let path = path.into();
self.add_variable(key, &path);
self.create_dirs.push(path);
}
}
fn to_ninja_target_string(
explicit: &[String],
implicit: &[String],
order_only: &[String],
) -> String {
let mut joined = space_separated(explicit);
if !implicit.is_empty() {
joined.push_str(" | ");
joined.push_str(&space_separated(implicit));
}
if !order_only.is_empty() {
joined.push_str(" || ");
joined.push_str(&space_separated(order_only));
}
joined
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn test_split_groups() {
assert_eq!(&split_groups("foo"), &["foo"]);
assert_eq!(&split_groups("foo:bar"), &["foo:bar", "foo"]);
assert_eq!(
&split_groups("foo:bar:baz"),
&["foo:bar:baz", "foo:bar", "foo"]
);
}
}