mirror of
https://github.com/ankitects/anki.git
synced 2025-09-21 23:42:23 -04:00

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.
566 lines
19 KiB
Rust
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"]
|
|
);
|
|
}
|
|
}
|