use anyhow::{Context, Result, anyhow}; use clap::{Parser, ValueEnum}; use inquire::{Confirm, Select, Text}; use regex::Regex; use std::fs; use std::path::Path; #[derive(Debug, Clone, ValueEnum)] enum Lang { C, Cpp, } #[derive(Debug, Clone, ValueEnum)] enum ProjectType { Exe, StaticLib, SharedLib, } #[derive(Debug, Clone, ValueEnum)] enum CxxStandard { #[value(name = "11")] Cxx11, #[value(name = "14")] Cxx14, #[value(name = "17")] Cxx17, #[value(name = "20")] Cxx20, #[value(name = "23")] Cxx23, #[value(name = "26")] Cxx26, } #[derive(Parser, Debug)] #[command(name = "ccmk")] #[command(author, version, about, long_about = None)] struct Args { /// Project name #[arg(short, long)] name: Option, /// Language: C or C++ #[arg(short, long, value_enum)] lang: Option, /// Project type: exe, static-lib, shared-lib #[arg(short, long, value_enum)] project_type: Option, /// C++ standard (for C++ projects) #[arg(short, long, value_enum)] cxx_standard: Option, /// Output directory #[arg(short, long)] output: Option, /// Force overwrite existing directory #[arg(long)] force: bool, } fn get_lang_ext(lang: &Lang) -> &'static str { match lang { Lang::C => "c", Lang::Cpp => "cpp", } } fn get_lang_cmake(lang: &Lang) -> &'static str { match lang { Lang::C => "C", Lang::Cpp => "CXX", } } fn generate_cmakelists( project_name: &str, lang: &Lang, project_type: &ProjectType, cxx_standard: &Option, ) -> String { let mut lines = vec![ "cmake_minimum_required(VERSION 3.20)".to_string(), format!( "project({} LANGUAGES {})", project_name, get_lang_cmake(lang) ), "".to_string(), ]; // 添加C++标准设置 if let Lang::Cpp = lang { let standard = match cxx_standard { Some(CxxStandard::Cxx11) => "11", Some(CxxStandard::Cxx14) => "14", Some(CxxStandard::Cxx17) => "17", Some(CxxStandard::Cxx20) => "20", Some(CxxStandard::Cxx23) => "23", Some(CxxStandard::Cxx26) => "26", None => "17", }; lines.push(format!("set(CMAKE_CXX_STANDARD {})", standard)); lines.push("set(CMAKE_CXX_STANDARD_REQUIRED ON)".to_string()); lines.push("set(CMAKE_CXX_EXTENSIONS OFF)".to_string()); lines.push("".to_string()); } // 处理不同项目类型 let ext = get_lang_ext(lang); match project_type { ProjectType::Exe => { lines.push(format!("add_executable({} src/main.{})", project_name, ext,)); } ProjectType::StaticLib => { lines.push(format!( "add_library({} STATIC src/main.{})", project_name, ext, )); lines.push(format!( "target_include_directories({} PUBLIC include)", project_name )); } ProjectType::SharedLib => { lines.push(format!( "add_library({} SHARED src/main.{})", project_name, ext, )); lines.push(format!( "target_include_directories({} PUBLIC include)", project_name )); lines.push(format!( "target_compile_definitions({} PUBLIC {}_EXPORTS)", project_name, project_name.to_uppercase().replace('-', "_") )); } } // 添加导出设置(仅库类型) if matches!( project_type, ProjectType::StaticLib | ProjectType::SharedLib ) { lines.push("".to_string()); lines.push("# Export targets".to_string()); lines.push("include(GNUInstallDirs)".to_string()); lines.push(format!("install(TARGETS {} DESTINATION lib)", project_name)); lines.push("install(DIRECTORY include/ DESTINATION include)".to_string()); } lines.join("\n") } fn generate_source_file(lang: &Lang, project_type: &ProjectType) -> String { match project_type { ProjectType::SharedLib => { match lang { Lang::C => { r#"#ifdef _WIN32 #include BOOL APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved) { switch (ul_reason_for_call) { case DLL_PROCESS_ATTACH: case DLL_THREAD_ATTACH: case DLL_THREAD_DETACH: case DLL_PROCESS_DETACH: break; } return TRUE; } #else // Non-Windows platforms void __attribute__((constructor)) lib_init(void) {} void __attribute__((destructor)) lib_fini(void) {} #endif"# .to_string() } Lang::Cpp => { r#"#include BOOL APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved) { switch (ul_reason_for_call) { case DLL_PROCESS_ATTACH: case DLL_THREAD_ATTACH: case DLL_THREAD_DETACH: case DLL_PROCESS_DETACH: break; } return TRUE; }"# .to_string() } } } _ => { match lang { Lang::C => { "#include \n\nint main() {\n printf(\"Hello, World!\\n\");\n return 0;\n}" .to_string() } Lang::Cpp => { "#include \n\nint main() {\n std::cout << \"Hello, World!\" << std::endl;\n return 0;\n}" .to_string() } } } } } fn sanitize_folder_name(name: &str) -> String { let re = Regex::new(r"[^a-zA-Z0-9_-]").unwrap(); let sanitized = re.replace_all(name, "").to_string(); if sanitized.is_empty() { return "cmake_project".to_string(); } sanitized } fn print_project_structure(folder_name: &str, ext: &str, project_type: &ProjectType) { println!("\n项目已成功创建!"); println!("📁 项目结构:"); println!(" - {}/", folder_name); println!(" ├── CMakeLists.txt"); println!(" ├── include/"); match project_type { ProjectType::Exe => { println!(" └── src/"); println!(" └── main.{}", ext); } ProjectType::StaticLib | ProjectType::SharedLib => { println!(" ├── src/"); println!(" │ └── main.{}", ext); println!(" └── README.md"); } } } fn interactive_mode() -> Result<(String, Lang, ProjectType, Option)> { let project_name = Text::new("请输入项目名称:").prompt()?; let lang_choices = vec!["C", "C++"]; let lang_raw = Select::new("请选择编程语言:", lang_choices).prompt()?; let lang = if lang_raw == "C" { Lang::C } else { Lang::Cpp }; let type_choices = vec!["Executable", "Static Library", "Shared Library"]; let type_raw = Select::new("请选择项目类型:", type_choices).prompt()?; let project_type = match type_raw { "Executable" => ProjectType::Exe, "Static Library" => ProjectType::StaticLib, "Shared Library" => ProjectType::SharedLib, _ => ProjectType::Exe, }; let cxx_standard = if matches!(lang, Lang::Cpp) { let standard_choices = vec!["11", "14", "17", "20", "23", "26"]; let standard_raw = Select::new("请选择C++标准版本:", standard_choices).prompt()?; Some(match standard_raw { "11" => CxxStandard::Cxx11, "14" => CxxStandard::Cxx14, "17" => CxxStandard::Cxx17, "20" => CxxStandard::Cxx20, "23" => CxxStandard::Cxx23, _ => CxxStandard::Cxx26, }) } else { None }; Ok((project_name, lang, project_type, cxx_standard)) } fn create_project( project_name: &str, lang: &Lang, project_type: &ProjectType, cxx_standard: &Option, output_dir: &Path, force: bool, ) -> Result<()> { // 检查目录是否存在 if output_dir.exists() { if force { println!("⚠️ 目录已存在,将被覆盖"); } else { let overwrite = Confirm::new(&format!( "目录 '{}' 已存在,是否覆盖?", output_dir.display() )) .prompt()?; if !overwrite { return Err(anyhow!("已取消")); } } } // 创建目录结构 fs::create_dir_all(output_dir.join("src"))?; if matches!( project_type, ProjectType::StaticLib | ProjectType::SharedLib ) { fs::create_dir_all(output_dir.join("include"))?; } // 写入 CMakeLists.txt fs::write( output_dir.join("CMakeLists.txt"), generate_cmakelists(project_name, lang, project_type, cxx_standard), ) .with_context(|| "写入 CMakeLists.txt 失败")?; // 写入源文件 let ext = get_lang_ext(lang); fs::write( output_dir.join("src").join(format!("main.{}", ext)), generate_source_file(lang, project_type), ) .with_context(|| "写入源文件失败")?; // 为库类型创建 README.md if matches!( project_type, ProjectType::StaticLib | ProjectType::SharedLib ) { let readme_content = format!( "# {}\n\nA CMake library project.\n\n## Build\n\n```bash\nmkdir build && cd build\ncmake ..\nmake\n```\n\n## Install\n\n```bash\ncmake --install .\n```\n", project_name ); fs::write(output_dir.join("README.md"), readme_content)?; } print_project_structure( &output_dir.file_name().unwrap().to_string_lossy(), ext, project_type, ); Ok(()) } fn main() -> Result<()> { let args = Args::parse(); // 如果没有提供任何参数,使用交互模式 if args.name.is_none() && args.lang.is_none() && args.project_type.is_none() && args.cxx_standard.is_none() { let (name, lang, project_type, cxx_standard) = interactive_mode()?; let folder_name = sanitize_folder_name(&name); let output_path = Path::new(".").join(&folder_name); create_project( &name, &lang, &project_type, &cxx_standard, &output_path, args.force, )?; return Ok(()); } // 命令行模式 let name = args.name.ok_or_else(|| anyhow!("项目名称为必填项"))?; let lang = args.lang.unwrap_or(Lang::Cpp); let project_type = args.project_type.unwrap_or(ProjectType::Exe); let cxx_standard = args.cxx_standard.or(Some(CxxStandard::Cxx17)); let folder_name = sanitize_folder_name(&name); let output_path = if let Some(output) = args.output { Path::new(&output).to_path_buf() } else { Path::new(".").join(&folder_name) }; create_project( &name, &lang, &project_type, &cxx_standard, &output_path, args.force, )?; Ok(()) }