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" ] } }
    }
  ]
}
相关推荐
松涛和鸣1 天前
DAY52 7-Segment Display/GPIO/Buttons/Interrupts/Timers/PWM
c语言·数据库·单片机·sqlite·html
曲幽1 天前
FastAPI数据库实战:从SQLAlchemy原理到高效连接管理,告别性能瓶颈
python·sqlite·flask·fastapi·web·sqlalchemy·db
一个平凡而乐于分享的小比特1 天前
Autoconf:Linux自动生成Makefile的详解
makefile·cmake·autoconf
八九燕来2 天前
django + drf 多表关联场景下的序列化器选型与实现逻辑
数据库·django·sqlite
谷哥的小弟2 天前
SQLite MCP服务器安装以及客户端连接配置
服务器·数据库·人工智能·sqlite·大模型·源码·mcp
xj7573065332 天前
《精通Django》第6章 Django表单
数据库·django·sqlite
hqp3 天前
SQLite 不支持 LocalDateTime
sqlite·mybatis
xj7573065333 天前
《精通Django》 第五章 Django管理后台
数据库·django·sqlite
张世争4 天前
windows clion MingW cmake 编译运行 FreeRTOS
windows·freertos·mingw·cmake·clion