CMake构建学习笔记31-构建前执行可执行程序

1. 引言

虽然 CMake 提供了非常多的构建指令来帮助程序的构建过程,但是这些构建指令不一定能满足实际的构建需求。遇到这种情况,就可以干脆自己写一个可执行程序,让 CMake 进行调用。

2. 实现

比如说,笔者有个需求是程序中有些代码是构建前生成的,或者需要在构建前进行更新。笔者的使用案例是将一个 SQLITE3 数据库中的表映射成枚举类,并且生成具体的代码文件:

cpp 复制代码
// 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来控制开启关闭:

cmake 复制代码
# 数据库结构生成工具
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):

cmake 复制代码
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配置:

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" ] } }
    }
  ]
}
相关推荐
羑悻的小杀马特15 小时前
百度ASR+BRPC+ETCD高并发高可用架构搭建,C++客户端API封装+语音识别子服务注册发现+全链路测试调优终极指南!
架构·sdk·etcd·cmake·百度智能云·brpc·语音识别子服务
_YouziTech_1 天前
VS Code环境下LVGL模拟器的配置与运行
visualstudio·mingw·cmake·lvgl·vs code·模拟器·sdl2
我的xiaodoujiao1 天前
Django 基础知识详细图文教程 4-Django 视图定义与使用
开发语言·数据库·后端·测试工具·django·sqlite
2601_962066492 天前
【玩转全栈】----Django模板的继承
数据库·django·sqlite
小北的AI科技分享2 天前
企业AI开发公司:数字化转型的关键力量
应用·构建·标注
张人玉2 天前
基于 Python + PyQt5 的水果目标检测与分割可视化系统——水果深度学习识别可视化大屏
python·深度学习·qt·目标检测·sqlite·echarts
派小汤3 天前
Harmony2.2.0通过RdbStore实现通用类操作本地SQLite数据库
数据库·sql·sqlite·鸿蒙·鸿蒙系统
用户3979678611164 天前
CMake-构建目标与属性
cmake
神仙别闹5 天前
基于C语言+SQLite 实现(控制台+图形界面)图书管理系统
c语言·开发语言·sqlite
迷迭香yy5 天前
A股数据本地存储方案对比JSON、SQLite、ClickHouse、DuckDB怎么选 IG50免费开源股票数据API接口
clickhouse·sqlite·json