From 5bb15cc69cdc997790f6482834b0e5323ff3ef62 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 3 Oct 2026 17:23:11 +0000 Subject: [PATCH] Bulk Migrate tab: Update CMake / Dry run CMake buttons + git bulk-update-cmake Runs psxrecomp's tools/update_cmake.py (sibling checkout, else the title's pinned submodule) on each ticked repo and commits CMakeLists.txt only. Honours the Push checkbox. --- src/studio/studio_main.cpp | 23 +++++++ .../project_studio/bulkops.py | 69 +++++++++++++++++++ .../new_project_layout/project_studio/cli.py | 39 +++++++++++ 3 files changed, 131 insertions(+) diff --git a/src/studio/studio_main.cpp b/src/studio/studio_main.cpp index d417e2a..7d0a0c2 100644 --- a/src/studio/studio_main.cpp +++ b/src/studio/studio_main.cpp @@ -3165,6 +3165,21 @@ void draw_bulk_migrate(StudioModel& model, const Theme& th) { retcomm::studio::run_project_studio_async(model, std::move(args), nullptr); }; + auto run_update_cmake = [&](bool dry_run) { + const std::string csv = select_csv(); + if (csv.empty()) { + model.append_log("[FAIL] No repos selected"); + return; + } + std::vector args = {"git", "bulk-update-cmake", "--select", csv, + "--jobs", std::to_string(model.bulk_jobs)}; + if (model.migrate_push) args.push_back("--push"); + if (dry_run) args.push_back("--dry-run"); + model.append_log(dry_run ? "--- Bulk update CMake (dry run) ---" + : "--- Bulk update CMake ---"); + retcomm::studio::run_project_studio_async(model, std::move(args), nullptr); + }; + if (bulk_btn("Select all")) { for (auto& kv : model.bulk_selected) kv.second = true; for (const auto& e : model.repos) model.bulk_selected[e.path] = true; @@ -3176,6 +3191,14 @@ void draw_bulk_migrate(StudioModel& model, const Theme& th) { const bool go = bulk_btn("Migrate selected"); accent_button_pop(); if (go) run_migrate(false); + if (bulk_btn("Update CMake")) run_update_cmake(false); + if (ImGui::IsItemHovered(ImGuiHoveredFlags_DelayNormal)) + ImGui::SetTooltip("Sync ticked titles' CMakeLists.txt with psxrecomp's scaffold template\n" + "(tools/update_cmake.py) and commit CMakeLists.txt only.\n" + "Honours Push."); + if (bulk_btn("Dry run CMake")) run_update_cmake(true); + if (ImGui::IsItemHovered(ImGuiHoveredFlags_DelayNormal)) + ImGui::SetTooltip("Report which ticked titles' CMakeLists.txt would change; writes nothing."); end_wrapped_line(); wrapped(th.text_muted, "Per-title results stream into the Activity log as [OK] / [FAIL] lines with the " diff --git a/tools/new_project_layout/project_studio/bulkops.py b/tools/new_project_layout/project_studio/bulkops.py index a2788cc..b12c55e 100644 --- a/tools/new_project_layout/project_studio/bulkops.py +++ b/tools/new_project_layout/project_studio/bulkops.py @@ -1052,6 +1052,75 @@ def one(label: str, root: Path) -> list[CmdResult]: return map_repos(repos, one, jobs=jobs, on_repo=on_repo) +def bulk_update_cmake( + repos: list[tuple[str, Path]], + *, + commit: bool = True, + push_remote: bool = False, + dry_run: bool = False, + jobs: int = 1, + on_repo: OnRepoResults | None = None, +) -> list[CmdResult]: + """PSX: sync each repo's CMakeLists.txt with psxrecomp's scaffold template. + + The work is psxrecomp's own ``tools/update_cmake.py`` (one owner, no copy + here): the sibling psxrecomp checkout's if there is one -- it carries the + newest managed blocks -- else the title's pinned ``psxrecomp/`` submodule. + Only CMakeLists.txt is committed.""" + import subprocess + import sys + + from . import platforms + from .paths import psxrecomp_root_from_toolkit + + if platforms.current().key != "psx": + return [CmdResult(False, "update-cmake: PSX titles only (psxrecomp scaffold)")] + + def one(label: str, root: Path) -> list[CmdResult]: + root = Path(root).expanduser().resolve() + if not (root / "CMakeLists.txt").is_file(): + return [CmdResult(False, f"{label}: no CMakeLists.txt")] + sibling = psxrecomp_root_from_toolkit() + script = next( + (c for c in ((sibling / "tools" / "update_cmake.py") if sibling else None, + root / "psxrecomp" / "tools" / "update_cmake.py") + if c is not None and c.is_file()), + None, + ) + if script is None: + return [CmdResult(False, f"{label}: no psxrecomp tools/update_cmake.py (sibling checkout or pinned submodule); update psxrecomp")] + + def run(*cmd: str) -> subprocess.CompletedProcess: + return subprocess.run(list(cmd), cwd=str(root), capture_output=True, text=True, + encoding="utf-8", errors="replace", check=False) + + r = run(sys.executable, str(script), "--project", str(root), "--check") + if r.returncode == 0: + return [CmdResult(True, f"{label}: CMakeLists.txt up to date")] + if r.returncode != 1: + return [CmdResult(False, f"{label}: update_cmake failed", (r.stdout + r.stderr).strip())] + if dry_run: + return [CmdResult(True, f"{label}: would update CMakeLists.txt", r.stdout.strip())] + r = run(sys.executable, str(script), "--project", str(root)) + if r.returncode != 0: + return [CmdResult(False, f"{label}: update_cmake failed", (r.stdout + r.stderr).strip())] + if not commit: + return [CmdResult(True, f"{label}: updated CMakeLists.txt (uncommitted)")] + r = run("git", "add", "--", "CMakeLists.txt") + if r.returncode == 0: + r = run("git", "commit", "-m", "CMake: sync managed blocks (update_cmake)", "--", "CMakeLists.txt") + if r.returncode != 0: + return [CmdResult(False, f"{label}: updated but commit failed", (r.stdout + r.stderr).strip())] + if push_remote: + pr = push(root) + if not pr.ok: + return [CmdResult(False, f"{label}: committed, push failed", pr.detail)] + return [CmdResult(True, f"{label}: updated, committed, pushed")] + return [CmdResult(True, f"{label}: updated + committed")] + + return map_repos(repos, one, jobs=jobs, on_repo=on_repo) + + def bulk_install_ci( repos: list[tuple[str, Path]], *, diff --git a/tools/new_project_layout/project_studio/cli.py b/tools/new_project_layout/project_studio/cli.py index 07d6400..21970d1 100644 --- a/tools/new_project_layout/project_studio/cli.py +++ b/tools/new_project_layout/project_studio/cli.py @@ -1854,6 +1854,34 @@ def on_repo(label: str, results: list) -> None: return 1 if failed else 0 +def cmd_git_bulk_update_cmake(args: argparse.Namespace) -> int: + from project_studio.bulkops import bulk_update_cmake + from project_studio.repo_index import load_index + + repos = _bulk_repos_or_die(args) + if repos is None: + return 2 + jobs = int(getattr(args, "jobs", 0) or 0) or int(getattr(load_index(), "bulk_jobs", 1) or 1) + + def on_repo(label: str, results: list) -> None: + for r in results: + print(f" [{'OK' if r.ok else 'FAIL'}] {r.message}", flush=True) + for ln in (r.detail or "").splitlines(): + print(f" {ln}", flush=True) + + results = bulk_update_cmake( + repos, + commit=not bool(getattr(args, "no_commit", False)), + push_remote=bool(getattr(args, "push", False)), + dry_run=bool(getattr(args, "dry_run", False)), + jobs=jobs, + on_repo=on_repo, + ) + failed = sum(1 for r in results if not r.ok) + print(f"bulk update-cmake: {len(results) - failed} ok, {failed} failed", flush=True) + return 1 if failed else 0 + + def cmd_git_release(args: argparse.Namespace) -> int: from project_studio.gitops import run_release_workflow @@ -3508,6 +3536,17 @@ def add_bulk_targets(p: argparse.ArgumentParser) -> None: ) p_gbci.set_defaults(func=cmd_git_bulk_install_ci) + p_gbuc = git_sub.add_parser( + "bulk-update-cmake", + help="PSX: sync selected repos' CMakeLists.txt with psxrecomp's scaffold template " + "via psxrecomp/tools/update_cmake.py (commits CMakeLists.txt only)", + ) + add_bulk_select(p_gbuc) + p_gbuc.add_argument("--no-commit", action="store_true", help="Edit CMakeLists.txt but do not commit") + p_gbuc.add_argument("--push", action="store_true", help="Push after committing") + p_gbuc.add_argument("--jobs", type=int, default=0, help="Parallel repos (default: index setting)") + p_gbuc.set_defaults(func=cmd_git_bulk_update_cmake) + p_gbmb = git_sub.add_parser( "bulk-migrate-bundled", help="PSX: move selected repos onto bundled releases (bump psxrecomp/recomp-ui, "