CMake构建学习笔记31-构建前执行可执行程序
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" ] } }
}
]
}

浙公网安备 33010602011771号