use anyhow::Result;
use colored::*;
use std::path::Path;
use walkdir::WalkDir;
use crate::build;
use crate::config::CxConfig;
#[derive(Clone, Debug)]
pub enum GenerateFormat {
Cmake,
Ninja,
CompileCommands,
}
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;
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)
"#
);
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));
}
}
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;
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
"#
));
}
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);
}
}
}
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(())
}