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>
#endifusing 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列是nameif (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 _WIN32SetConsoleOutputCP(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_generatoradd_custom_command(OUTPUT ${RUN_MARKER}COMMAND $<TARGET_FILE:db_schema_generator> ${SQLITE_DB_PATH} ${GENERATED_DIR} # 运行刚编译的 exeCOMMAND ${CMAKE_COMMAND} -E touch ${RUN_MARKER} # 创建标记文件DEPENDS db_schema_generator # 必须先构建生成器COMMENT "Running DbSchemaGenerator..."VERBATIM)# 创建一个自定义目标,代表“已运行生成器”add_custom_target(run_db_generator ALLDEPENDS ${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" ] } }}]
}