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" ] } }
    }
  ]
}
posted @ 2025-12-25 09:07  charlee44  阅读(5)  评论(0)    收藏  举报