409inline int apply_regex(std::string &src,
const std::string &pattern,
410 const std::string &replacement,
const std::string &flags,
411 const std::string &desc, std::vector<Change> &out) {
412 auto opts = std::regex::ECMAScript;
413 if (flags.find(
'i') != std::string::npos)
414 opts |= std::regex::icase;
415 std::regex re(pattern, opts);
416 auto first = std::sregex_iterator(src.begin(), src.end(), re);
417 int count =
static_cast<int>(std::distance(first, std::sregex_iterator()));
419 src = std::regex_replace(src, re, replacement);
420 out.push_back({0, desc});
449 const std::string &new_tag, std::vector<Change> &out) {
450 std::regex header(
"FetchContent_(?:Populate|Declare)\\s*\\(\\s*" + block +
453 if (!std::regex_search(cmake, m, header))
456 const std::size_t n = cmake.size();
457 std::size_t i = m.position(0) + m.length(0);
459 bool saw_tag =
false;
460 std::size_t val_begin = std::string::npos, val_end = std::string::npos;
462 while (i < n && depth > 0) {
463 const char c = cmake[i];
465 while (i < n && cmake[i] !=
'\n')
470 const std::size_t q_begin = ++i;
471 while (i < n && cmake[i] !=
'"') {
472 if (cmake[i] ==
'\\' && i + 1 < n)
476 const std::size_t q_end = i;
486 if (c ==
'(') { ++depth; ++i;
continue; }
487 if (c ==
')') { --depth; ++i;
continue; }
488 if (std::isspace(
static_cast<unsigned char>(c))) { ++i;
continue; }
491 const std::size_t tok_begin = i;
492 while (i < n && !std::isspace(
static_cast<unsigned char>(cmake[i])) &&
493 cmake[i] !=
'(' && cmake[i] !=
')' && cmake[i] !=
'#' &&
497 val_begin = tok_begin;
501 if (cmake.compare(tok_begin, i - tok_begin,
"GIT_TAG") == 0)
505 if (val_begin == std::string::npos)
507 if (cmake.compare(val_begin, val_end - val_begin, new_tag) == 0)
510 cmake.replace(val_begin, val_end - val_begin, new_tag);
511 out.push_back({line, block +
" GIT_TAG -> " + new_tag});
616inline int run(
const fs::path &project_dir,
const fs::path &migrations_dir,
617 const fs::path &deps_manifest,
const Options &opts) {
618 using namespace rang;
620 fs::path cmake_path = project_dir /
"CMakeLists.txt";
622 cmake.
path = cmake_path;
624 std::cerr << fg::red <<
"Error: no CMakeLists.txt in " << project_dir
625 << fg::reset << std::endl;
634 <<
"Error: cannot detect the plugin protocol (no `GIT_TAG "
635 "v*-P<N>` in the plugin FetchContent block). Use --from to "
637 << fg::reset << std::endl;
646 std::string manifest_text;
647 if (
read_file(deps_manifest, manifest_text)) {
649 target = json::parse(manifest_text).value(
"plugin_protocol", -1);
650 }
catch (
const std::exception &) {
653 if (target < 0 && !steps.empty())
654 target = steps.rbegin()->second.to;
657 std::cerr << fg::red <<
"Error: cannot determine a target protocol."
658 << fg::reset << std::endl;
662 std::cout << style::bold <<
"Plugin migration: " << project_dir.filename().string()
663 << style::reset << std::endl;
664 std::cout <<
" Detected protocol: " << fg::yellow <<
"P" << cur << fg::reset
665 <<
" Target: " << fg::green <<
"P" << target << fg::reset
669 std::cout << fg::green <<
" Already at protocol P" << cur
670 <<
" (>= target). Nothing to do." << fg::reset << std::endl;
675 for (
int v = cur; v < target; ++v) {
676 if (steps.find(v) == steps.end()) {
677 std::cerr << fg::red <<
"Error: no migration step for P" << v <<
" -> P"
678 << (v + 1) <<
" in " << migrations_dir << fg::reset << std::endl;
684 std::vector<FileReport> sources;
685 fs::path src_dir = project_dir /
"src";
686 if (fs::is_directory(src_dir)) {
687 for (
const auto &e : fs::directory_iterator(src_dir)) {
688 auto ext = e.path().extension().string();
689 if (ext ==
".cpp" || ext ==
".cxx" || ext ==
".cc" || ext ==
".hpp" ||
695 sources.push_back(std::move(fr));
702 std::vector<std::string> notes;
703 for (
int v = cur; v < target; ++v) {
704 const json &step = steps[v].doc;
705 std::cout <<
" Applying step " << style::bold <<
"P" << v <<
" -> P"
706 << (v + 1) << style::reset <<
" (" << steps[v].file.filename().string()
709 for (
auto &fr : sources)
711 if (step.contains(
"notes"))
712 for (
const auto &nnote : step[
"notes"])
713 notes.push_back(nnote.get<std::string>());
720 std::cout <<
" " << style::bold << fs::relative(fr.path, project_dir).string()
721 << style::reset <<
":" << std::endl;
722 for (
const auto &ch : fr.changes) {
723 std::cout <<
" " << fg::green <<
"✓" << fg::reset <<
" " << ch.detail;
725 std::cout << fg::gray <<
" (line " << ch.line <<
")" << fg::reset;
726 std::cout << std::endl;
729 write_file(fr.path.string() +
".bak", fr.original);
734 int edited_files = 0;
738 for (
auto &fr : sources) {
744 if (edited_files == 0) {
745 std::cout << fg::yellow
746 <<
" No matching code found to change (already migrated?)."
747 << fg::reset << std::endl;
749 std::cout << std::endl
750 << fg::yellow << style::bold <<
"Dry run: " << style::reset
751 << fg::yellow <<
"no files written (" << edited_files
752 <<
" would change)." << fg::reset << std::endl;
754 std::cout << std::endl
755 << fg::green <<
"Migrated " << edited_files
756 <<
" file(s); originals saved as *.bak." << fg::reset << std::endl;
759 if (!notes.empty()) {
760 std::cout << std::endl << style::bold <<
"MANUAL FOLLOW-UP:" << style::reset
762 for (
const auto &nnote : notes)
763 std::cout <<
" " << fg::yellow <<
"•" << fg::reset <<
" " << nnote
770 std::cout << std::endl
771 << style::bold <<
"Verifying (cmake build)…" << style::reset
773 fs::path build_dir = project_dir /
"build";
775 std::string cfg =
"cmake -S \"" + project_dir.string() +
"\" -B \"" +
776 build_dir.string() +
"\"";
777 std::string bld =
"cmake --build \"" + build_dir.string() +
"\"";
782 std::cout <<
" " << fg::green <<
"✓ Plugin compiles cleanly against P"
783 << target <<
"." << fg::reset << std::endl;
785 std::cout <<
" " << fg::red
786 <<
"✗ Build failed — the rewrite likely needs manual "
787 "fixes. Compiler errors:"
788 << fg::reset << std::endl;
789 std::istringstream iss(out);
792 while (std::getline(iss, ln) && shown < 40) {
793 std::string low = ln;
794 std::transform(low.begin(), low.end(), low.begin(), ::tolower);
795 if (low.find(
"error") != std::string::npos) {
796 std::cout <<
" " << ln << std::endl;
800 std::cout << fg::gray <<
" (full log under " << build_dir.string() <<
")"
801 << fg::reset << std::endl;