caxe 0.3.8

A modern C/C++ project manager that cuts through build system complexity. Zero config, smart dependencies, and parallel builds.
Documentation
//! Generate command handler
//!
//! Handles `cx generate` subcommands for generating build system files.

use anyhow::Result;
use colored::*;
use std::path::Path;
use walkdir::WalkDir;

use crate::build;
use crate::config::CxConfig;

/// Generate format options
#[derive(Clone, Debug)]
pub enum GenerateFormat {
    /// Generate CMakeLists.txt
    Cmake,
    /// Generate build.ninja
    Ninja,
    /// Generate compile_commands.json
    CompileCommands,
}

/// Handle the `cx generate` command for build system file generation
pub fn handle_generate_command(format: &GenerateFormat) -> Result<()> {
    let config = build::load_config()?;

    match format {
        GenerateFormat::Cmake => {
            generate_cmake(&config)?;
        }
        GenerateFormat::Ninja => {
            generate_ninja(&config)?;
        }
        GenerateFormat::CompileCommands => {
            println!(
                "{} compile_commands.json is generated automatically when building.",
                "!".yellow()
            );
            println!("   Location: {}", ".cx/build/compile_commands.json".cyan());
            println!("   Run {} to generate it.", "cx build".cyan());
        }
    }
    Ok(())
}

fn generate_cmake(config: &CxConfig) -> Result<()> {
    println!("{} Generating CMakeLists.txt...", "📝".cyan());

    let name = &config.package.name;
    let edition = &config.package.edition;

    // Convert edition to CMake standard
    let cpp_standard = edition.replace("c++", "").replace("c", "");

    let mut cmake = format!(
        r#"cmake_minimum_required(VERSION 3.16)
project({name} LANGUAGES CXX)

set(CMAKE_CXX_STANDARD {cpp_standard})
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)

# Source files
file(GLOB_RECURSE SOURCES "src/*.cpp" "src/*.c")

# Executable
add_executable(${{PROJECT_NAME}} ${{SOURCES}})

# Include directories
target_include_directories(${{PROJECT_NAME}} PRIVATE src)
"#
    );

    // Add dependencies if present
    if let Some(deps) = &config.dependencies {
        cmake.push_str("\n# Dependencies\n");
        for dep_name in deps.keys() {
            cmake.push_str(&format!("# find_package({} REQUIRED)\n", dep_name));
        }
    }

    // Add libs if present
    if let Some(build) = &config.build
        && let Some(libs) = &build.libs
    {
        cmake.push_str("\n# Libraries\ntarget_link_libraries(${PROJECT_NAME} PRIVATE");
        for lib in libs {
            cmake.push_str(&format!(" {}", lib));
        }
        cmake.push_str(")\n");
    }

    std::fs::write("CMakeLists.txt", cmake)?;
    println!("{} Created CMakeLists.txt", "".green());
    println!();
    println!("Usage:");
    println!(
        "   {} && {}",
        "cmake -B build -S .".yellow(),
        "cmake --build build".yellow()
    );

    Ok(())
}

fn generate_ninja(config: &CxConfig) -> Result<()> {
    println!("{} Generating build.ninja...", "📝".cyan());

    let name = &config.package.name;
    let edition = &config.package.edition;

    // Detect compiler
    let compiler = if cfg!(windows) { "cl" } else { "g++" };
    let is_msvc = compiler == "cl";

    let std_flag = if is_msvc {
        build::utils::get_std_flag_msvc(edition)
    } else {
        build::utils::get_std_flag_gcc(edition)
    };

    let mut ninja = String::from("# Auto-generated by caxe\n\n");

    if is_msvc {
        ninja.push_str(&format!(
            r#"
cxx = cl
cxxflags = /nologo /EHsc {std_flag} /c
linkflags = /nologo

rule compile
  command = $cxx $cxxflags $in /Fo$out
  description = Compiling $in

rule link
  command = $cxx $linkflags $in /Fe$out
  description = Linking $out

"#
        ));
    } else {
        ninja.push_str(&format!(
            r#"
cxx = g++
cxxflags = {std_flag} -c
linkflags = 

rule compile
  command = $cxx $cxxflags $in -o $out
  description = Compiling $in

rule link
  command = $cxx $linkflags $in -o $out
  description = Linking $out

"#
        ));
    }

    // Find source files
    let src_dir = Path::new("src");
    let mut obj_files = Vec::new();

    if src_dir.exists() {
        for entry in WalkDir::new(src_dir).into_iter().filter_map(|e| e.ok()) {
            let path = entry.path();
            if path
                .extension()
                .and_then(|e| e.to_str())
                .is_some_and(|e| ["cpp", "cc", "cxx", "c"].contains(&e))
            {
                let obj_name = path.file_stem().unwrap_or_default().to_string_lossy();
                let obj_ext = if is_msvc { "obj" } else { "o" };
                let obj_path = format!("build/{}.{}", obj_name, obj_ext);

                ninja.push_str(&format!("build {}: compile {}\n", obj_path, path.display()));
                obj_files.push(obj_path);
            }
        }
    }

    // Link
    let exe_ext = if cfg!(windows) { ".exe" } else { "" };
    ninja.push_str(&format!(
        "\nbuild build/{}{}: link {}\n",
        name,
        exe_ext,
        obj_files.join(" ")
    ));
    ninja.push_str(&format!("\ndefault build/{}{}\n", name, exe_ext));

    std::fs::write("build.ninja", ninja)?;
    println!("{} Created build.ninja", "".green());
    println!();
    println!("Usage: {}", "ninja".yellow());

    Ok(())
}