如何将CMake构建前执行可执行程序成?

摘要:通过一个 数据库结构生成器 的示例,说明了如何在 CMake 构建过程前按需自动生成 C++ 代码。
1. 引言 虽然 CMake 提供了非常多的构建指令来帮助程序的构建过程,但是这些构建指令不一定能满足实际的构建需求。遇到这种情况,就可以干脆自己写一个可执行程序,让 CMake 进行调用。 2. 实现 比如说,笔者有个需求是程序中有些代码是构建前生成的,或者需要在构建前进行更新。笔者的使用案例是将一个 SQLITE3 数据库中的表映射成枚举类,并且生成具体的代码文件: // Script/DbSchemaGenerator.cpp #include <sqlite3.h> #include <filesystem> #include <fstream> #include <iostream> #include <string> #include <vector> #ifdef _WIN32 #include <Windows.h> #endif using namespace std; //转换成帕斯卡命名 std::string ToPascalCase(const std::string& input) { if (input.empty()) { return ""; } std::string result; bool nextUpper = true; // 下一个有效字符应大写 for (char c : input) { if (c == '_') { // 遇到下划线,下一个非下划线字母要大写 nextUpper = true; } else { if (nextUpper) { result += static_cast<char>(std::toupper(static_cast<unsigned char>(c))); nextUpper = false; } else { result += static_cast<char>(std::tolower(static_cast<unsigned char>(c))); } } } // 如果结果为空(比如输入全是下划线),返回空串 return result; } vector<string> QueryTableName(sqlite3* db) { vector<string> tableNames; // 获取所有用户表 const char* sqlTables = "SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE " "'sqlite_%';"; sqlite3_stmt* stmtTables; int rc = sqlite3_prepare_v2(db, sqlTables, -1, &stmtTables, nullptr); if (rc != SQLITE_OK) { std::cerr << "Failed to fetch tables: " << sqlite3_errmsg(db) << "\n"; return tableNames; } while (sqlite3_step(stmtTables) == SQLITE_ROW) { const char* tableNameCstr = reinterpret_cast<const char*>(sqlite3_column_text(stmtTables, 0)); if (!tableNameCstr) continue; tableNames.emplace_back(tableNameCstr); } sqlite3_finalize(stmtTables); return tableNames; } string Read2String(filesystem::path& filePath) { std::ifstream infile(filePath); if (!infile) { return {}; } return {(std::istreambuf_iterator<char>(infile)), std::istreambuf_iterator<char>()}; } void WriteTableName(filesystem::path& tableNameFile, const vector<string>& tableNames) { std::ostringstream memStream; memStream << "#pragma once\n"; memStream << "\n"; memStream << "namespace Persistence {\n"; memStream << "\n"; memStream << "enum class TableName {\n"; for (size_t i = 0; i < tableNames.size(); ++i) { string line; if (i == tableNames.size() - 1) { line = std::format(" {}\n", tableNames[i]); } else { line = std::format(" {},\n", tableNames[i]); } memStream << line; } memStream << "};\n"; memStream << "\n"; memStream << "}"; if (memStream.str() == Read2String(tableNameFile)) { return; } ofstream file(tableNameFile); if (!file) { std::cerr << "Failed to open file '" << tableNameFile.generic_string() << "' for writing.\n"; return; } file << memStream.str(); } vector<string> QueryFiledName(sqlite3* db, const string& tableName) { vector<string> filedNames; const string& sql = "PRAGMA table_info(" + tableName + ");"; sqlite3_stmt* stmt; int rc = sqlite3_prepare_v2(db, sql.c_str(), -1, &stmt, nullptr); if (rc != SQLITE_OK) { std::cerr << "Failed to get schema for table '" << tableName.c_str() << "': " << sqlite3_errmsg(db) << "\n"; return filedNames; } while (sqlite3_step(stmt) == SQLITE_ROW) { const char* col_name = reinterpret_cast<const char*>( sqlite3_column_text(stmt, 1)); // 第1列是name if (col_name) { filedNames.emplace_back(col_name); } } sqlite3_finalize(stmt); return filedNames; } void WriteFiledName(filesystem::path& outSourceDir, const string& fileName, const vector<string>& filedNames) { std::ostringstream memStream; memStream << "#pragma once\n"; memStream << "\n"; memStream << "namespace Persistence {\n"; memStream << "\n"; memStream << std::format("enum class {} {{\n", fileName); for (size_t i = 0; i < filedNames.size(); ++i) { string line; if (i == filedNames.size() - 1) { line = std::format(" {}\n", filedNames[i]); } else { line = std::format(" {},\n", filedNames[i]); } memStream << line; } memStream << "};\n"; memStream << "\n"; memStream << "}"; filesystem::path filedNameFile = outSourceDir / (fileName + ".h"); if (memStream.str() == Read2String(filedNameFile)) { return; } ofstream file(filedNameFile); if (!file) { std::cerr << "Failed to open file '" << filedNameFile.generic_string() << "' for writing.\n"; return; } file << memStream.str(); } int main(int argc, char* argv[]) { #ifdef _WIN32 SetConsoleOutputCP(65001); #endif // if (argc != 3) { std::cerr << "Usage: " << argv[0] << " <database_path> <output_directory>\n"; return 1; } // const char* dbPath = argv[1]; const char* outputDir = argv[2]; std::cout << "Generating DB schema enums...\n"; std::cout << " DB Path: " << dbPath << "\n"; std::cout << " Output : " << outputDir << "\n"; filesystem::path outSourceDir{outputDir}; sqlite3* db; int rc = sqlite3_open(dbPath, &db); if (rc != SQLITE_OK) { std::cerr << "Cannot open database: " << sqlite3_errmsg(db) << "\n"; sqlite3_close(db); return 1; } vector<string> tableNames = QueryTableName(db); filesystem::path tableNameFile = outSourceDir / "TableName.h"; WriteTableName(tableNameFile, tableNames); for (auto tableName : tableNames) { string fileName = "Table" + ToPascalCase(tableName) + "Field"; WriteFiledName(outSourceDir, fileName, QueryFiledName(db, tableName)); } sqlite3_close(db); return 0; } 当然,这个功能每次构建程序的时候都调用没有必要,将其设置成ENABLE_DB_SCHEMA_GENERATION来控制开启关闭: # 数据库结构生成工具 option(ENABLE_DB_SCHEMA_GENERATION "Enable automatic generation of database schema headers" OFF) if(ENABLE_DB_SCHEMA_GENERATION) add_subdirectory(Script) endif() 当开启这个构建选项ENABLE_DB_SCHEMA_GENERATION,就通过add_custom_command来添加自定义命令,创建一个自定义目标(add_custom_target),构建主程序前先运行这个目标指定的自定义命令(add_dependencies): if(ENABLE_DB_SCHEMA_GENERATION) # 用户可配置的数据库路径(缓存变量) set(SQLITE_DB_PATH "" CACHE FILEPATH "Path to source SQLite database for code generation") if(NOT EXISTS "${SQLITE_DB_PATH}") message(FATAL_ERROR "Database file not found: ${SQLITE_DB_PATH}") endif() # 设置数据库路径 set(GENERATED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/Persistence") # 创建一个“标记文件”,用于 CMake 跟踪是否已运行 set(RUN_MARKER "${CMAKE_BINARY_DIR}/.db_generator_ran") # 生成文件输出目录 file(MAKE_DIRECTORY ${GENERATED_DIR}) # 定义:运行 db_schema_generator add_custom_command( OUTPUT ${RUN_MARKER} COMMAND $<TARGET_FILE:db_schema_generator> ${SQLITE_DB_PATH} ${GENERATED_DIR} # 运行刚编译的 exe COMMAND ${CMAKE_COMMAND} -E touch ${RUN_MARKER} # 创建标记文件 DEPENDS db_schema_generator # 必须先构建生成器 COMMENT "Running DbSchemaGenerator..." VERBATIM ) # 创建一个自定义目标,代表“已运行生成器” add_custom_target(run_db_generator ALL DEPENDS ${RUN_MARKER} ) # 让主程序依赖这个目标 → 构建主程序前会先运行生成器 add_dependencies(charlee-blog-backend run_db_generator) message(STATUS "DB schema generation ENABLED. Using database: ${SQLITE_DB_PATH}") else() message(STATUS "DB schema generation DISABLED (set -DENABLE_DB_SCHEMA_GENERATION=ON to enable)") endif() 对应的CMakePresets.json配置: { "version": 2, "configurePresets": [ { "name": "RelWithDebInfo", "displayName": "Windows x64 RelWithDebInfo Shared Library", "description": "面向具有 Visual Studio 开发环境的 Windows。", "generator": "Ninja", "binaryDir": "${sourceDir}/out/build/${presetName}", "architecture": { "value": "x64", "strategy": "external" }, "cacheVariables": { "CMAKE_BUILD_TYPE": "RelWithDebInfo", "CMAKE_PREFIX_PATH": "$env{GISBasic}", "CMAKE_INSTALL_PREFIX": "$env{GISBasic}", "ENABLE_DB_SCHEMA_GENERATION": true, "SQLITE_DB_PATH": "${sourceDir}/../charlee-blog-db.sqlite3" }, "vendor": { "microsoft.com/VisualStudioSettings/CMake/1.0": { "hostOS": [ "Windows" ] } } } ] }