use crate::models::project_meta::{CompressedMakefile, MakeTarget};
use regex::Regex;
use std::collections::{HashMap, HashSet};
use tracing::debug;
pub struct MakefileCompressor {
critical_targets: HashSet<&'static str>,
critical_vars: HashSet<&'static str>,
var_pattern: Regex,
target_pattern: Regex,
}
impl MakefileCompressor {
#[must_use]
pub fn new() -> Self {
let mut critical_targets = HashSet::new();
critical_targets.insert("all");
critical_targets.insert("build");
critical_targets.insert("test");
critical_targets.insert("install");
critical_targets.insert("clean");
critical_targets.insert("release");
critical_targets.insert("fmt");
critical_targets.insert("format");
critical_targets.insert("lint");
critical_targets.insert("check");
critical_targets.insert("coverage");
critical_targets.insert("docs");
critical_targets.insert("deploy");
critical_targets.insert("run");
critical_targets.insert("serve");
critical_targets.insert("dev");
critical_targets.insert("prod");
critical_targets.insert("dist");
critical_targets.insert("package");
let mut critical_vars = HashSet::new();
critical_vars.insert("PROJECT_NAME");
critical_vars.insert("VERSION");
critical_vars.insert("CC");
critical_vars.insert("CXX");
critical_vars.insert("CARGO");
critical_vars.insert("RUSTC");
critical_vars.insert("PYTHON");
critical_vars.insert("NODE");
critical_vars.insert("NPM");
critical_vars.insert("DOCKER");
critical_vars.insert("KUBECTL");
critical_vars.insert("CFLAGS");
critical_vars.insert("LDFLAGS");
critical_vars.insert("TARGET");
critical_vars.insert("ARCH");
critical_vars.insert("OS");
Self {
critical_targets,
critical_vars,
var_pattern: Regex::new(r"^([A-Z_][A-Z0-9_]*)\s*[:?]?=").unwrap(),
target_pattern: Regex::new(r"^([a-zA-Z0-9_\-\.]+):").unwrap(),
}
}
pub fn compress(&self, content: &str) -> CompressedMakefile {
let mut result = CompressedMakefile::default();
for line in content.lines() {
let trimmed = line.trim();
if let Some(caps) = self.var_pattern.captures(trimmed) {
let var_name = caps.get(1).unwrap().as_str();
if self.critical_vars.contains(var_name) || var_name.starts_with("PROJECT_") {
result.variables.push(line.to_string());
}
}
}
let targets = self.parse_targets(content);
for (name, target) in targets {
if self.is_critical_target(&name) {
result.targets.push(MakeTarget {
name: name.clone(),
deps: target.dependencies,
recipe_summary: self.summarize_recipe(&target.recipe),
});
}
}
result.detected_toolchain = self.detect_toolchain(content, &result.targets);
result.key_dependencies = self.extract_dependencies(content);
debug!(
"Compressed Makefile: {} variables, {} targets",
result.variables.len(),
result.targets.len()
);
result
}
fn parse_targets(&self, content: &str) -> HashMap<String, ParsedTarget> {
let mut targets = HashMap::new();
let mut current_target: Option<String> = None;
let mut in_recipe = false;
for line in content.lines() {
if line.trim().starts_with('#') {
continue;
}
if let Some(caps) = self.target_pattern.captures(line) {
let target_name = caps.get(1).unwrap().as_str().to_string();
let deps = line
.split(':')
.nth(1)
.map(|d| {
d.split_whitespace()
.map(std::string::ToString::to_string)
.collect::<Vec<_>>()
})
.unwrap_or_default();
targets.insert(
target_name.clone(),
ParsedTarget {
dependencies: deps,
recipe: Vec::new(),
},
);
current_target = Some(target_name);
in_recipe = true;
} else if in_recipe {
if line.starts_with('\t') || line.starts_with(" ") {
if let Some(ref target) = current_target {
if let Some(t) = targets.get_mut(target) {
t.recipe.push(line.to_string());
}
}
} else if !line.trim().is_empty() {
in_recipe = false;
current_target = None;
}
}
}
targets
}
fn is_critical_target(&self, name: &str) -> bool {
self.critical_targets.contains(name)
|| name.starts_with("docker")
|| name.starts_with("test-")
|| name.starts_with("build-")
|| name.contains("deploy")
|| name.contains("install")
}
fn summarize_recipe(&self, recipe_lines: &[String]) -> String {
for line in recipe_lines {
let trimmed = line.trim_start_matches('\t').trim_start_matches(' ');
let clean = trimmed.trim_start_matches('@').trim_start_matches('-');
if !clean.starts_with("echo ")
&& !clean.starts_with("mkdir ")
&& !clean.starts_with("rm ")
&& !clean.starts_with(':')
&& !clean.is_empty()
{
if clean.len() > 100 {
return format!("{}...", &clean[..97]);
}
return clean.to_string();
}
}
"[complex recipe]".to_string()
}
fn detect_toolchain(&self, content: &str, targets: &[MakeTarget]) -> Option<String> {
let content_lower = content.to_lowercase();
if content_lower.contains("cargo ") || content_lower.contains("rustc") {
return Some("rust".to_string());
}
if content_lower.contains("python") || content_lower.contains("pip") {
return Some("python".to_string());
}
if content_lower.contains("npm ") || content_lower.contains("node ") {
return Some("node".to_string());
}
if content_lower.contains("go build") || content_lower.contains("go test") {
return Some("go".to_string());
}
if content_lower.contains("gcc")
|| content_lower.contains("g++")
|| content_lower.contains("clang")
{
return Some("c/c++".to_string());
}
if content_lower.contains("javac")
|| content_lower.contains("mvn")
|| content_lower.contains("gradle")
{
return Some("java".to_string());
}
for target in targets {
let recipe_lower = target.recipe_summary.to_lowercase();
if recipe_lower.contains("cargo") {
return Some("rust".to_string());
}
if recipe_lower.contains("python") {
return Some("python".to_string());
}
if recipe_lower.contains("npm") || recipe_lower.contains("node") {
return Some("node".to_string());
}
}
None
}
fn extract_dependencies(&self, content: &str) -> Vec<String> {
let mut deps = HashSet::new();
for line in content.lines() {
let lower = line.to_lowercase();
if lower.contains("cargo install") {
if let Some(pkg) = extract_package_name(&lower, "cargo install") {
deps.insert(pkg);
}
}
if lower.contains("npm install") || lower.contains("npm i ") {
if let Some(pkg) = extract_package_name(&lower, "npm install") {
deps.insert(pkg);
}
}
if lower.contains("apt-get install") || lower.contains("apt install") {
if let Some(pkg) = extract_package_name(&lower, "install") {
deps.insert(pkg);
}
}
for cmd in &["docker", "kubectl", "terraform", "ansible", "make", "cmake"] {
if lower.contains(&format!("command -v {cmd}"))
|| lower.contains(&format!("which {cmd}"))
{
deps.insert((*cmd).to_string());
}
}
}
let mut result: Vec<String> = deps.into_iter().collect();
result.sort();
result.truncate(10); result
}
}
impl Default for MakefileCompressor {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug)]
struct ParsedTarget {
dependencies: Vec<String>,
recipe: Vec<String>,
}
fn extract_package_name(line: &str, after: &str) -> Option<String> {
let parts: Vec<&str> = line.split_whitespace().collect();
let install_pos = find_install_position(&parts, after)?;
find_package_at_position(&parts, install_pos)
}
fn find_install_position(parts: &[&str], after: &str) -> Option<usize> {
match after {
"cargo install" => find_cargo_install_position(parts),
"npm install" => find_npm_install_position(parts),
"install" => find_simple_install_position(parts),
_ => None,
}
}
fn find_cargo_install_position(parts: &[&str]) -> Option<usize> {
parts.iter().position(|&p| p == "cargo")?;
parts
.iter()
.position(|&p| p == "install")
.map(|pos| pos + 1)
}
fn find_npm_install_position(parts: &[&str]) -> Option<usize> {
parts.iter().position(|&p| p == "npm")?;
parts
.iter()
.position(|&p| p == "install")
.map(|pos| pos + 1)
}
fn find_simple_install_position(parts: &[&str]) -> Option<usize> {
parts
.iter()
.position(|&p| p == "install")
.map(|pos| pos + 1)
}
fn find_package_at_position(parts: &[&str], position: usize) -> Option<String> {
if let Some(pkg) = get_valid_package(parts, position) {
return Some(pkg);
}
get_valid_package(parts, position + 1)
}
fn get_valid_package(parts: &[&str], position: usize) -> Option<String> {
parts
.get(position)
.filter(|pkg| !pkg.starts_with('-') && !pkg.is_empty())
.map(|pkg| (*pkg).to_string())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_compress_basic_makefile() {
let content = r#"
PROJECT_NAME = myproject
VERSION = 1.0.0
CC = gcc
RANDOM_VAR = something
all: build test
build:
$(CC) -o $(PROJECT_NAME) main.c
test:
./run_tests.sh
clean:
rm -rf build/
install:
cp $(PROJECT_NAME) /usr/local/bin/
"#;
let compressor = MakefileCompressor::new();
let result = compressor.compress(content);
assert_eq!(result.variables.len(), 3);
assert!(result.variables.iter().any(|v| v.contains("PROJECT_NAME")));
assert!(result.variables.iter().any(|v| v.contains("VERSION")));
assert!(result.variables.iter().any(|v| v.contains("CC")));
assert!(!result.variables.iter().any(|v| v.contains("RANDOM_VAR")));
let target_names: Vec<&str> = result.targets.iter().map(|t| t.name.as_str()).collect();
assert!(target_names.contains(&"all"));
assert!(target_names.contains(&"build"));
assert!(target_names.contains(&"test"));
assert!(target_names.contains(&"clean"));
assert!(target_names.contains(&"install"));
assert_eq!(result.targets.len(), 5);
let all_target = result.targets.iter().find(|t| t.name == "all").unwrap();
assert_eq!(all_target.deps, vec!["build", "test"]);
let build_target = result.targets.iter().find(|t| t.name == "build").unwrap();
assert!(build_target
.recipe_summary
.contains("$(CC) -o $(PROJECT_NAME) main.c"));
assert_eq!(result.detected_toolchain, Some("c/c++".to_string()));
}
#[test]
fn test_compress_rust_makefile() {
let content = r#"
CARGO = cargo
RUSTFLAGS = -D warnings
build:
$(CARGO) build --release
test:
$(CARGO) test --all
test-integration:
$(CARGO) test --test integration_tests
docker-build:
docker build -t myapp .
deploy:
kubectl apply -f k8s/
"#;
let compressor = MakefileCompressor::new();
let result = compressor.compress(content);
assert_eq!(result.detected_toolchain, Some("rust".to_string()));
assert!(result.variables.iter().any(|v| v.contains("CARGO")));
let target_names: Vec<&str> = result.targets.iter().map(|t| t.name.as_str()).collect();
assert!(target_names.contains(&"build"));
assert!(target_names.contains(&"test"));
assert!(target_names.contains(&"test-integration"));
assert!(target_names.contains(&"docker-build"));
assert!(target_names.contains(&"deploy"));
}
#[test]
fn test_recipe_summarization() {
let content = r#"
verbose:
@echo "Starting build..."
@mkdir -p build/
$(CC) -Wall -Werror -O2 -pthread -lm -ldl -o build/app src/*.c src/utils/*.c src/core/*.c -Iinclude/ -Llib/ -lexternal
@echo "Build complete!"
simple:
cargo build
"#;
let compressor = MakefileCompressor::new();
let targets = compressor.parse_targets(content);
let verbose_recipe = &targets.get("verbose").unwrap().recipe;
let summary = compressor.summarize_recipe(verbose_recipe);
assert!(summary.starts_with("$(CC) -Wall -Werror"));
assert!(summary.ends_with("..."));
assert!(summary.len() <= 100);
let simple_recipe = &targets.get("simple").unwrap().recipe;
let simple_summary = compressor.summarize_recipe(simple_recipe);
assert_eq!(simple_summary, "cargo build");
}
#[test]
fn test_dependency_extraction() {
let content = r#"
setup:
command -v docker || echo "Docker not installed"
which kubectl || echo "kubectl not installed"
cargo install sccache
npm install -g typescript
apt-get install -y build-essential
deps:
pip install -r requirements.txt
go get github.com/some/package
"#;
let compressor = MakefileCompressor::new();
let result = compressor.compress(content);
assert!(result.key_dependencies.contains(&"docker".to_string()));
assert!(result.key_dependencies.contains(&"kubectl".to_string()));
assert!(result.key_dependencies.contains(&"sccache".to_string()));
assert!(result.key_dependencies.contains(&"typescript".to_string()));
assert!(result
.key_dependencies
.contains(&"build-essential".to_string()));
}
#[test]
fn test_toolchain_detection() {
let test_cases = vec![
("cargo test\ncargo build", Some("rust")),
("python setup.py\npip install", Some("python")),
("npm run build\nnode index.js", Some("node")),
("go build ./...\ngo test", Some("go")),
("gcc -o app\ng++ -std=c++17", Some("c/c++")),
("javac Main.java\nmvn package", Some("java")),
("echo 'no toolchain'", None),
];
let compressor = MakefileCompressor::new();
for (content, expected) in test_cases {
let result = compressor.compress(content);
assert_eq!(
result.detected_toolchain,
expected.map(|s| s.to_string()),
"Failed for content: {content}"
);
}
}
}
#[cfg(test)]
mod property_tests {
use proptest::prelude::*;
proptest! {
#[test]
fn basic_property_stability(_input in ".*") {
prop_assert!(true);
}
#[test]
fn module_consistency_check(_x in 0u32..1000) {
prop_assert!(_x < 1001);
}
}
}