extern crate die;
extern crate getopts;
extern crate unmake;
extern crate walkdir;
use self::unmake::{inspect, warnings};
use die::{Die, die};
use std::env;
use std::fs;
use std::io;
use std::path;
use std::process;
use std::sync;
pub static DIRECTORY_EXCLUSIONS: sync::LazyLock<Vec<&str>> =
sync::LazyLock::new(|| vec![".git", "node_modules", "vendor"]);
fn main() {
let brief: String = format!(
"Usage: {} <OPTIONS> <path> [<path> ...]",
env!("CARGO_PKG_NAME")
);
let mut opts: getopts::Options = getopts::Options::new();
opts.optopt("i", "inspect", "summarize file details", "<makefile>");
opts.optflag("d", "debug", "emit additional logs");
opts.optflag("h", "help", "print usage info");
opts.optflag("l", "list", "list makefile paths");
opts.optflag("", "print0", "null delimit paths");
opts.optflag(
"n",
"dry-run",
"process makefiles through external build tools",
);
opts.optflag("v", "version", "print version info");
let usage: String = opts.usage(&brief);
let arguments: Vec<String> = env::args().collect();
let optmatches: getopts::Matches = opts.parse(&arguments[1..]).die(&usage);
if optmatches.opt_present("h") {
die!(0; usage);
}
if optmatches.opt_present("v") {
die!(0; format!("{} {}", env!("CARGO_PKG_NAME"), env!("CARGO_PKG_VERSION")));
}
let debug: bool = optmatches.opt_present("d");
let list_makefile_paths: bool = optmatches.opt_present("l");
let null_delimit_paths: bool = optmatches.opt_present("print0");
let process_dry_run: bool = optmatches.opt_present("n");
if optmatches.opt_present("i") {
let pth_string = optmatches.opt_str("i").die(&usage);
let pth: &path::Path = path::Path::new(&pth_string);
let metadata: inspect::Metadata = inspect::analyze(pth).map_err(|err| die!(err)).unwrap();
println!("{}", metadata);
die!(0);
}
let pth_strings: Vec<String> = optmatches.free;
if pth_strings.is_empty() {
die!(1; usage);
}
let mut found_quirk = false;
let mut ws: Vec<warnings::Warning> = Vec::new();
let cwd: std::path::PathBuf =
env::current_dir().die("error: unable to query current working directory");
let mut action = |p: &path::Path| {
let pth_string: String = p.display().to_string();
let metadata_result: Result<unmake::inspect::Metadata, String> =
unmake::inspect::analyze(p);
if let Err(err) = &metadata_result {
found_quirk = true;
println!("{}", err);
return;
}
let metadata: inspect::Metadata = metadata_result.unwrap();
if !metadata.is_makefile {
return;
}
if metadata.is_machine_generated {
if debug {
eprintln!(
"debug: skipping {}: likely machine-generated by {}",
pth_string, metadata.build_system
);
}
return;
}
if list_makefile_paths {
if null_delimit_paths {
print!("{}\0", pth_string);
} else {
println!("{}", pth_string);
}
return;
}
if process_dry_run {
if metadata.is_include_file {
if debug {
eprintln!(
"debug: skipping include makefile for dry run analysis: {}",
pth_string
);
}
return;
}
let dir: &std::path::Path = p
.parent()
.map(|e| match e.display().to_string().as_str() {
"" => cwd.as_path(),
_ => e,
})
.unwrap_or(cwd.as_path());
let dry_run_output: process::Output = process::Command::new(&metadata.build_system)
.args(["-nf", &metadata.filename])
.current_dir(dir)
.output()
.die(
&format!(
"error: unable to run build tool: {}",
&metadata.build_system
)
.to_string(),
);
if !dry_run_output.status.success() {
found_quirk = true;
println!("{}", pth_string);
print!(
"{}",
String::from_utf8(dry_run_output.stdout).unwrap_or(
format!(
"error: unable to decode {} stdout stream",
&metadata.build_system
)
.to_string()
)
);
eprint!(
"{}",
String::from_utf8(dry_run_output.stderr).unwrap_or(
format!(
"error: unable to decode {} stderr stream",
&metadata.build_system
)
.to_string()
)
);
}
return;
}
if metadata.build_system != "make" {
if debug {
eprintln!(
"debug: skipping {}: non-strict implementation {}",
pth_string, metadata.build_system
);
}
return;
}
let makefile_str_result: Result<String, io::Error> = fs::read_to_string(p);
if let Err(err) = &makefile_str_result {
found_quirk = true;
println!("error: {}: {}", p.display(), err);
return;
}
let makefile_str: &str = &makefile_str_result.unwrap();
let ws2_result: Result<Vec<warnings::Warning>, String> =
warnings::lint(&metadata, makefile_str);
if let Err(err) = ws2_result {
found_quirk = true;
println!("{}", err);
return;
}
let ws2: Vec<warnings::Warning> = ws2_result.unwrap();
if !ws2.is_empty() {
found_quirk = true;
}
ws.extend(ws2);
};
for pth_string in pth_strings {
let pth: &path::Path = path::Path::new(&pth_string);
if pth.is_dir() {
let walker = walkdir::WalkDir::new(pth)
.sort_by_file_name()
.into_iter()
.filter_entry(|e| {
!DIRECTORY_EXCLUSIONS.contains(&e.file_name().to_str().unwrap_or(""))
});
for entry_result in walker {
let entry: walkdir::DirEntry = entry_result.unwrap();
let child_pth: &path::Path = entry.path();
if child_pth.is_dir() || child_pth.is_symlink() {
continue;
}
action(child_pth);
}
} else {
action(pth);
}
}
ws.sort_by(|a, b| a.line.cmp(&b.line));
for w in ws {
println!("{}", w);
}
if found_quirk {
die!(1);
}
}