use rucc_pp::Dependency;
use rucc_session::Deps;
const WIDTH: usize = 72;
#[must_use]
pub fn escaped(name: &str) -> String {
let mut out = String::with_capacity(name.len());
for ch in name.chars() {
match ch {
' ' | '#' => {
out.push('\\');
out.push(ch);
}
'$' => out.push_str("$$"),
_ => out.push(ch),
}
}
out
}
fn write_name(out: &mut String, column: &mut usize, name: &str) {
let width = name.chars().count();
if *column > 0 && *column + 1 + width > WIDTH {
out.push_str(" \\\n ");
*column = 1;
} else if *column > 0 {
out.push(' ');
*column += 1;
}
out.push_str(name);
*column += width;
}
#[must_use]
pub fn default_target(source: &str, output: Option<&str>) -> String {
if let Some(name) = output {
return escaped(name);
}
escaped(&with_suffix(base_name(source), "o"))
}
#[must_use]
pub fn default_file(opts: &Deps, source: &str, output: Option<&str>) -> Option<String> {
if let Some(name) = &opts.file {
return Some(name.clone());
}
if opts.instead_of_compiling {
return None;
}
Some(match output {
Some(name) => with_suffix(name, "d"),
None => with_suffix(base_name(source), "d"),
})
}
fn with_suffix(name: &str, suffix: &str) -> String {
let start = name.rfind(['/', '\\']).map_or(0, |at| at + 1);
match name[start..].rfind('.') {
Some(dot) => format!("{}{suffix}", &name[..start + dot + 1]),
None => format!("{name}.{suffix}"),
}
}
fn base_name(name: &str) -> &str {
match name.rfind(['/', '\\']) {
Some(at) => &name[at + 1..],
None => name,
}
}
#[must_use]
pub fn rule(opts: &Deps, targets: &[String], source: &str, found: &[Dependency]) -> String {
let listed: Vec<String> = found
.iter()
.filter(|dep| opts.system_headers || !dep.is_system)
.map(|dep| escaped(&dep.path.to_string_lossy()))
.collect();
let mut out = String::new();
let mut column = 0;
for (at, target) in targets.iter().enumerate() {
if at > 0 {
out.push(' ');
column += 1;
}
out.push_str(target);
column += target.chars().count();
}
out.push(':');
column += 1;
write_name(&mut out, &mut column, &escaped(source));
for name in &listed {
write_name(&mut out, &mut column, name);
}
out.push('\n');
if opts.phony {
for name in &listed {
out.push_str(name);
out.push_str(":\n");
}
}
out
}
#[cfg(test)]
mod tests {
use std::path::PathBuf;
use super::*;
fn dep(path: &str, is_system: bool) -> Dependency {
Dependency { path: PathBuf::from(path), is_system }
}
fn plain() -> Deps {
Deps { emit: true, system_headers: true, ..Deps::default() }
}
#[test]
fn the_source_is_the_first_prerequisite_and_the_headers_follow_it() {
let found = [dep("sub/loc.h", false), dep("sub/deep.h", false)];
let text = rule(&plain(), &["a.o".to_owned()], "sub/a.c", &found);
assert_eq!(text, "a.o: sub/a.c sub/loc.h sub/deep.h\n");
}
#[test]
fn a_system_header_is_dropped_when_the_flag_said_to_drop_it() {
let found = [dep("/usr/include/stdio.h", true), dep("sub/loc.h", false)];
let opts = Deps { emit: true, system_headers: false, ..Deps::default() };
assert_eq!(rule(&opts, &["a.o".to_owned()], "a.c", &found), "a.o: a.c sub/loc.h\n");
assert_eq!(
rule(&plain(), &["a.o".to_owned()], "a.c", &found),
"a.o: a.c /usr/include/stdio.h sub/loc.h\n"
);
}
#[test]
fn the_characters_make_reads_are_escaped_the_way_make_reads_them() {
let found = [dep("sp ace.h", false), dep("ha#sh.h", false), dep("dol$lar.h", false)];
let text = rule(&plain(), &["q.o".to_owned()], "q.c", &found);
assert_eq!(text, "q.o: q.c sp\\ ace.h ha\\#sh.h dol$$lar.h\n");
}
#[test]
fn a_long_rule_wraps_where_gcc_wraps_it() {
let found = [
dep("/usr/include/stdc-predef.h", true),
dep("/usr/include/stdio.h", true),
dep("/usr/include/x86_64-linux-gnu/bits/libc-header-start.h", true),
dep("/usr/include/features.h", true),
];
let text = rule(&plain(), &["a.o".to_owned()], "sub/a.c", &found);
assert_eq!(
text,
"a.o: sub/a.c /usr/include/stdc-predef.h /usr/include/stdio.h \\\n \
/usr/include/x86_64-linux-gnu/bits/libc-header-start.h \\\n \
/usr/include/features.h\n"
);
for line in text.lines() {
assert!(line.chars().count() <= WIDTH + 2, "{line} is wider than the wrap allows");
}
}
#[test]
fn a_name_wider_than_the_line_is_written_anyway() {
let long = format!("/{}.h", "d".repeat(WIDTH * 2));
let found = [dep(&long, false)];
let text = rule(&plain(), &["a.o".to_owned()], "a.c", &found);
assert_eq!(text, format!("a.o: a.c \\\n {long}\n"));
}
#[test]
fn a_phony_target_is_added_for_every_prerequisite_except_the_source() {
let found = [dep("loc.h", false), dep("/usr/include/stdio.h", true)];
let opts = Deps { emit: true, system_headers: false, phony: true, ..Deps::default() };
assert_eq!(rule(&opts, &["a.o".to_owned()], "a.c", &found), "a.o: a.c loc.h\nloc.h:\n");
}
#[test]
fn a_file_that_includes_nothing_gets_no_phony_targets() {
let opts = Deps { emit: true, phony: true, ..Deps::default() };
assert_eq!(rule(&opts, &["n.o".to_owned()], "n.c", &[]), "n.o: n.c\n");
}
#[test]
fn more_than_one_target_shares_the_one_colon() {
let text = rule(&plain(), &["one".to_owned(), "two".to_owned()], "a.c", &[]);
assert_eq!(text, "one two: a.c\n");
}
#[test]
fn the_target_is_the_output_file_where_there_is_one_and_the_source_otherwise() {
assert_eq!(default_target("sub/a.c", None), "a.o");
assert_eq!(default_target("sub/a.c", Some("sub/obj.o")), "sub/obj.o");
assert_eq!(default_target("sub/a.c", Some("prog")), "prog");
assert_eq!(default_target("a.c", Some("out dir/a.o")), "out\\ dir/a.o");
}
#[test]
fn the_rule_goes_beside_the_output_unless_it_replaces_the_compilation() {
let write = Deps { emit: true, ..Deps::default() };
assert_eq!(default_file(&write, "sub/a.c", None).as_deref(), Some("a.d"));
assert_eq!(
default_file(&write, "sub/a.c", Some("sub/obj.o")).as_deref(),
Some("sub/obj.d")
);
assert_eq!(
default_file(&write, "sub/a.c", Some("objnoext")).as_deref(),
Some("objnoext.d")
);
let print = Deps { emit: true, instead_of_compiling: true, ..Deps::default() };
assert_eq!(default_file(&print, "sub/a.c", None), None);
let named = Deps { file: Some("named.dep".to_owned()), ..print.clone() };
assert_eq!(default_file(&named, "sub/a.c", None).as_deref(), Some("named.dep"));
}
#[test]
fn a_suffix_is_replaced_only_where_the_last_component_has_one() {
assert_eq!(with_suffix("a.c", "d"), "a.d");
assert_eq!(with_suffix("dir.v2/a", "d"), "dir.v2/a.d");
assert_eq!(with_suffix("dir.v2/a.c", "d"), "dir.v2/a.d");
assert_eq!(with_suffix("a.tar.gz", "d"), "a.tar.d");
}
}