|
| 1 | +// SPDX-License-Identifier: Apache-2.0 |
| 2 | + |
| 3 | +#include "SplitFileWriter.hpp" |
| 4 | + |
| 5 | +#include <format> |
| 6 | +#include <fstream> |
| 7 | +#include <stdexcept> |
| 8 | +#include <system_error> |
| 9 | + |
| 10 | +namespace Lightweight::CodeGen |
| 11 | +{ |
| 12 | + |
| 13 | +std::vector<std::vector<CodeBlock>> GroupBlocksByLineBudget(std::vector<CodeBlock> const& blocks, |
| 14 | + std::size_t maxLinesPerFile) |
| 15 | +{ |
| 16 | + std::vector<std::vector<CodeBlock>> chunks; |
| 17 | + if (blocks.empty()) |
| 18 | + return chunks; |
| 19 | + |
| 20 | + if (maxLinesPerFile == 0) |
| 21 | + { |
| 22 | + chunks.emplace_back(blocks); |
| 23 | + return chunks; |
| 24 | + } |
| 25 | + |
| 26 | + chunks.emplace_back(); |
| 27 | + std::size_t currentLines = 0; |
| 28 | + for (auto const& block: blocks) |
| 29 | + { |
| 30 | + if (!chunks.back().empty() && currentLines + block.lineCount > maxLinesPerFile) |
| 31 | + { |
| 32 | + chunks.emplace_back(); |
| 33 | + currentLines = 0; |
| 34 | + } |
| 35 | + chunks.back().push_back(block); |
| 36 | + currentLines += block.lineCount; |
| 37 | + } |
| 38 | + if (chunks.back().empty()) |
| 39 | + chunks.pop_back(); |
| 40 | + return chunks; |
| 41 | +} |
| 42 | + |
| 43 | +namespace |
| 44 | +{ |
| 45 | + /// @brief Ensures the parent directory exists so subsequent `ofstream` opens succeed. |
| 46 | + void EnsureParentDirectoryExists(std::filesystem::path const& filePath) |
| 47 | + { |
| 48 | + auto const parent = filePath.parent_path(); |
| 49 | + if (parent.empty()) |
| 50 | + return; |
| 51 | + std::error_code ec; |
| 52 | + std::filesystem::create_directories(parent, ec); |
| 53 | + if (ec) |
| 54 | + throw std::runtime_error(std::format("Failed to create directory {}: {}", parent.string(), ec.message())); |
| 55 | + } |
| 56 | + |
| 57 | + /// @brief Writes one chunk file with optional header/footer. |
| 58 | + void WriteChunkFile(std::filesystem::path const& path, |
| 59 | + std::vector<CodeBlock> const& chunk, |
| 60 | + std::string_view header, |
| 61 | + std::string_view footer) |
| 62 | + { |
| 63 | + EnsureParentDirectoryExists(path); |
| 64 | + std::ofstream out(path); |
| 65 | + if (!out.is_open()) |
| 66 | + throw std::runtime_error(std::format("Failed to open output file: {}", path.string())); |
| 67 | + if (!header.empty()) |
| 68 | + out << header; |
| 69 | + for (auto const& block: chunk) |
| 70 | + out << block.content; |
| 71 | + if (!footer.empty()) |
| 72 | + out << footer; |
| 73 | + } |
| 74 | + |
| 75 | + /// @brief Total line count of all blocks; cached on the `CodeBlock` so this loop |
| 76 | + /// is just a sum, not a per-call newline scan. |
| 77 | + [[nodiscard]] std::size_t TotalLines(std::vector<CodeBlock> const& blocks) |
| 78 | + { |
| 79 | + std::size_t total = 0; |
| 80 | + for (auto const& b: blocks) |
| 81 | + total += b.lineCount; |
| 82 | + return total; |
| 83 | + } |
| 84 | +} // namespace |
| 85 | + |
| 86 | +WriteResult EmitChunked(std::filesystem::path const& outputPath, |
| 87 | + std::vector<CodeBlock> const& blocks, |
| 88 | + std::size_t maxLinesPerFile, |
| 89 | + std::string_view fileHeader, |
| 90 | + std::string_view fileFooter) |
| 91 | +{ |
| 92 | + WriteResult result; |
| 93 | + |
| 94 | + if (maxLinesPerFile == 0 || TotalLines(blocks) <= maxLinesPerFile) |
| 95 | + { |
| 96 | + WriteChunkFile(outputPath, blocks, fileHeader, fileFooter); |
| 97 | + result.writtenFiles.push_back(outputPath); |
| 98 | + return result; |
| 99 | + } |
| 100 | + |
| 101 | + auto const chunks = GroupBlocksByLineBudget(blocks, maxLinesPerFile); |
| 102 | + auto const stem = outputPath.parent_path() / outputPath.stem(); |
| 103 | + auto const ext = outputPath.extension().string(); |
| 104 | + |
| 105 | + for (std::size_t i = 0; i < chunks.size(); ++i) |
| 106 | + { |
| 107 | + auto partPath = std::filesystem::path { std::format("{}_part{:02}{}", stem.string(), i + 1, ext) }; |
| 108 | + WriteChunkFile(partPath, chunks[i], fileHeader, fileFooter); |
| 109 | + result.writtenFiles.push_back(std::move(partPath)); |
| 110 | + } |
| 111 | + |
| 112 | + return result; |
| 113 | +} |
| 114 | + |
| 115 | +void EmitPluginCmake(std::filesystem::path const& outputDir, std::string_view pluginName, std::string_view sourceGlob) |
| 116 | +{ |
| 117 | + std::error_code ec; |
| 118 | + std::filesystem::create_directories(outputDir, ec); |
| 119 | + if (ec) |
| 120 | + throw std::runtime_error(std::format("Failed to create directory {}: {}", outputDir.string(), ec.message())); |
| 121 | + |
| 122 | + auto const cmakePath = outputDir / "CMakeLists.txt"; |
| 123 | + { |
| 124 | + std::ofstream out(cmakePath); |
| 125 | + if (!out.is_open()) |
| 126 | + throw std::runtime_error(std::format("Failed to open output file: {}", cmakePath.string())); |
| 127 | + out << "# SPDX-License-Identifier: Apache-2.0\n" |
| 128 | + << "# Auto-generated. DO NOT EDIT.\n" |
| 129 | + << "\n" |
| 130 | + << "cmake_minimum_required(VERSION 3.25)\n" |
| 131 | + << "\n" |
| 132 | + << "# Pick up every generated migration source. CONFIGURE_DEPENDS makes CMake re-glob\n" |
| 133 | + << "# when the generator regenerates the directory, so new sources enter the build\n" |
| 134 | + << "# without a manual reconfigure.\n" |
| 135 | + << "file(GLOB " << pluginName << "_MIGRATIONS CONFIGURE_DEPENDS\n" |
| 136 | + << " \"${CMAKE_CURRENT_SOURCE_DIR}/" << sourceGlob << "\"\n" |
| 137 | + << ")\n" |
| 138 | + << "\n" |
| 139 | + << "add_library(" << pluginName << " MODULE\n" |
| 140 | + << " Plugin.cpp\n" |
| 141 | + << " ${" << pluginName << "_MIGRATIONS}\n" |
| 142 | + << ")\n" |
| 143 | + << "\n" |
| 144 | + << "target_link_libraries(" << pluginName << " PRIVATE Lightweight::Lightweight)\n" |
| 145 | + << "\n" |
| 146 | + << "set_target_properties(" << pluginName << " PROPERTIES\n" |
| 147 | + << " LIBRARY_OUTPUT_DIRECTORY \"${CMAKE_BINARY_DIR}/plugins\"\n" |
| 148 | + << " RUNTIME_OUTPUT_DIRECTORY \"${CMAKE_BINARY_DIR}/plugins\"\n" |
| 149 | + << " PREFIX \"\"\n" |
| 150 | + << ")\n" |
| 151 | + << "\n" |
| 152 | + << "# Generated migration sources are large and intentionally literal; skip clang-tidy\n" |
| 153 | + << "# so lint thresholds (function size, cognitive complexity) don't trip on them.\n" |
| 154 | + << "set_target_properties(" << pluginName << " PROPERTIES CXX_CLANG_TIDY \"\")\n" |
| 155 | + << "set_source_files_properties(${" << pluginName << "_MIGRATIONS} PROPERTIES SKIP_LINTING TRUE)\n"; |
| 156 | + } |
| 157 | + |
| 158 | + auto const pluginPath = outputDir / "Plugin.cpp"; |
| 159 | + { |
| 160 | + std::ofstream out(pluginPath); |
| 161 | + if (!out.is_open()) |
| 162 | + throw std::runtime_error(std::format("Failed to open output file: {}", pluginPath.string())); |
| 163 | + out << "// SPDX-License-Identifier: Apache-2.0\n" |
| 164 | + << "// Auto-generated. DO NOT EDIT.\n" |
| 165 | + << "//\n" |
| 166 | + << "// Migration plugin entry point. Individual migrations self-register with the\n" |
| 167 | + << "// MigrationManager via static initialization; this file only exposes the\n" |
| 168 | + << "// plugin ABI that dbtool expects when dlopen'ing the shared module.\n" |
| 169 | + << "\n" |
| 170 | + << "#include <Lightweight/SqlMigration.hpp>\n" |
| 171 | + << "\n" |
| 172 | + << "LIGHTWEIGHT_MIGRATION_PLUGIN()\n"; |
| 173 | + } |
| 174 | +} |
| 175 | + |
| 176 | +} // namespace Lightweight::CodeGen |
0 commit comments