use super::ast::*;
#[derive(Debug, Clone, Default)]
pub struct MakefileGeneratorOptions {
pub preserve_formatting: bool,
pub max_line_length: Option<usize>,
pub skip_blank_line_removal: bool,
pub skip_consolidation: bool,
}
pub fn generate_purified_makefile(ast: &MakeAst) -> String {
generate_purified_makefile_with_options(ast, &MakefileGeneratorOptions::default())
}
pub fn generate_purified_makefile_with_options(
ast: &MakeAst,
options: &MakefileGeneratorOptions,
) -> String {
let mut output = String::new();
let mut prev_was_comment = false;
for (idx, item) in ast.items.iter().enumerate() {
let item_output = generate_item(item, options);
let should_add_blank_line =
should_preserve_blank_line(item, idx > 0, prev_was_comment, options);
if should_add_blank_line && idx > 0 {
output.push('\n'); }
let formatted_output = if let Some(max_len) = options.max_line_length {
apply_line_length_limit(&item_output, max_len)
} else {
item_output
};
output.push_str(&formatted_output);
output.push('\n');
prev_was_comment = matches!(item, MakeItem::Comment { .. });
}
output
}
fn should_preserve_blank_line(
item: &MakeItem,
has_prev: bool,
prev_was_comment: bool,
options: &MakefileGeneratorOptions,
) -> bool {
if !has_prev {
return false;
}
if options.preserve_formatting || options.skip_blank_line_removal {
match item {
MakeItem::Comment { .. } if !prev_was_comment => true,
MakeItem::Target { .. } => true,
_ => false,
}
} else {
false
}
}
fn apply_line_length_limit(text: &str, max_length: usize) -> String {
let mut result = String::new();
for line in text.lines() {
if line.len() <= max_length {
result.push_str(line);
result.push('\n');
} else {
let mut current_line = String::new();
let leading_tabs = line.chars().take_while(|c| *c == '\t').count();
let indent = "\t".repeat(leading_tabs);
current_line.push_str(&indent);
let mut current_len = indent.len();
let content = &line[leading_tabs..];
for word in content.split_whitespace() {
let word_len = word.len() + 1;
if current_len + word_len > max_length && current_len > indent.len() {
result.push_str(¤t_line);
if !current_line.ends_with('\\') {
result.push_str(" \\");
}
result.push('\n');
current_line.clear();
current_line.push_str(&indent);
current_line.push(' '); current_len = indent.len() + 1;
}
if !current_line.ends_with(&indent) && !current_line.ends_with(' ') {
current_line.push(' ');
current_len += 1;
}
current_line.push_str(word);
current_len += word.len();
}
if !current_line.trim().is_empty() {
result.push_str(¤t_line);
result.push('\n');
}
}
}
result.trim_end_matches('\n').to_string()
}
fn generate_item(item: &MakeItem, options: &MakefileGeneratorOptions) -> String {
match item {
MakeItem::Variable {
name,
value,
flavor,
..
} => generate_variable(name, value, flavor),
MakeItem::Target {
name,
prerequisites,
recipe,
phony,
recipe_metadata,
..
} => generate_target(
name,
prerequisites,
recipe,
*phony,
recipe_metadata.as_ref(),
options,
),
MakeItem::PatternRule {
target_pattern,
prereq_patterns,
recipe,
recipe_metadata,
..
} => generate_pattern_rule(
target_pattern,
prereq_patterns,
recipe,
recipe_metadata.as_ref(),
options,
),
MakeItem::Conditional {
condition,
then_items,
else_items,
..
} => generate_conditional(condition, then_items, else_items.as_deref(), options),
MakeItem::Include { path, optional, .. } => generate_include(path, *optional),
MakeItem::Comment { text, .. } => generate_comment(text),
MakeItem::FunctionCall { name, args, .. } => {
format!("$({} {})", name, args.join(","))
}
}
}
fn generate_variable(name: &str, value: &str, flavor: &VarFlavor) -> String {
format!("{} {} {}", name, flavor, value)
}
fn generate_target(
name: &str,
prerequisites: &[String],
recipe: &[String],
phony: bool,
recipe_metadata: Option<&RecipeMetadata>,
options: &MakefileGeneratorOptions,
) -> String {
let mut output = String::new();
if phony {
output.push_str(&format!(".PHONY: {}\n", name));
}
output.push_str(name);
output.push(':');
if !prerequisites.is_empty() {
output.push(' ');
output.push_str(&prerequisites.join(" "));
}
output.push('\n');
if let Some(metadata) = recipe_metadata {
if (options.preserve_formatting || options.skip_consolidation) && !recipe.is_empty() {
for line in recipe {
output.push_str(&reconstruct_recipe_line_with_breaks(line, metadata));
}
} else {
for line in recipe {
output.push('\t');
output.push_str(line);
output.push('\n');
}
}
} else {
for line in recipe {
output.push('\t');
output.push_str(line);
output.push('\n');
}
}
output.pop();
output
}
fn reconstruct_recipe_line_with_breaks(line: &str, metadata: &RecipeMetadata) -> String {
if metadata.line_breaks.is_empty() {
return format!("\t{}\n", line);
}
let mut output = String::new();
output.push('\t');
let line_bytes = line.as_bytes();
let mut last_pos = 0;
for (break_pos, original_indent) in &metadata.line_breaks {
if *break_pos <= line.len() {
let text_segment = &line[last_pos..*break_pos];
let trimmed_segment = text_segment.trim_end();
output.push_str(trimmed_segment);
output.push_str(" \\");
output.push('\n');
output.push_str(original_indent);
last_pos = *break_pos;
if last_pos < line_bytes.len() && line_bytes[last_pos] == b' ' {
last_pos += 1; }
}
}
if last_pos < line.len() {
output.push_str(&line[last_pos..]);
}
output.push('\n');
output
}
fn generate_pattern_rule(
target_pattern: &str,
prereq_patterns: &[String],
recipe: &[String],
recipe_metadata: Option<&RecipeMetadata>,
options: &MakefileGeneratorOptions,
) -> String {
generate_target(
target_pattern,
prereq_patterns,
recipe,
false,
recipe_metadata,
options,
)
}
fn generate_conditional(
condition: &MakeCondition,
then_items: &[MakeItem],
else_items: Option<&[MakeItem]>,
options: &MakefileGeneratorOptions,
) -> String {
let mut output = String::new();
match condition {
MakeCondition::IfEq(left, right) => {
output.push_str(&format!("ifeq ({},{})\n", left, right));
}
MakeCondition::IfNeq(left, right) => {
output.push_str(&format!("ifneq ({},{})\n", left, right));
}
MakeCondition::IfDef(var) => {
output.push_str(&format!("ifdef {}\n", var));
}
MakeCondition::IfNdef(var) => {
output.push_str(&format!("ifndef {}\n", var));
}
}
for item in then_items {
output.push_str(&generate_item(item, options));
output.push('\n');
}
if let Some(else_items) = else_items {
output.push_str("else\n");
for item in else_items {
output.push_str(&generate_item(item, options));
output.push('\n');
}
}
output.push_str("endif");
output
}
fn generate_include(path: &str, optional: bool) -> String {
if optional {
format!("-include {}", path)
} else {
format!("include {}", path)
}
}
fn generate_comment(text: &str) -> String {
format!("# {}", text)
}
#[cfg(test)]
#[path = "generators_tests_span.rs"]
mod tests_extracted;