project studio

This commit is contained in:
Alex Vanderveen
2026-08-11 23:17:47 -04:00
parent 2207cd9a09
commit 37a5b7c7cd
6 changed files with 1275 additions and 144 deletions
+3
View File
@@ -128,3 +128,6 @@ runtime/tests/test_*
# transient split-gen build dirs
recompiler/build-splitgen/
recompiler/build-*/
# Project Studio GUI venv (customtkinter)
tools/new_project_layout/.venv/
+28 -2
View File
@@ -14,7 +14,14 @@ See [`docs/GAME_PROJECT_SETUP.md`](../../docs/GAME_PROJECT_SETUP.md).
## Project Studio (migrate / update)
Shared Python library under `project_studio/` with CLI + tkinter GUI.
Shared Python library under `project_studio/` with CLI + CustomTkinter GUI.
CLI stays stdlib-only. First GUI launch auto-creates
`tools/new_project_layout/.venv` and installs `requirements-gui.txt`
(network once); later launches are silent.
```bash
python3 tools/new_project_layout/project_studio_gui.py
```
```bash
# Audit layout gaps
@@ -34,12 +41,31 @@ python3 tools/new_project_layout/migrate_project.py apply \
--root /path/to/ApeEscapeRecomp \
--disc /path/to/game.cue
# GUI
# GUI (auto-bootstraps .venv + customtkinter on first run)
python3 tools/new_project_layout/migrate_project.py gui
# or
python3 tools/new_project_layout/project_studio_gui.py
# Git / GitHub (also available as the GUI "Git / GitHub" tab)
python3 tools/new_project_layout/migrate_project.py git status \
--root /path/to/ApeEscapeRecomp
python3 tools/new_project_layout/migrate_project.py git ensure-submodules \
--root /path/to/ApeEscapeRecomp \
--psxrecomp-branch master --recomp-ui-branch master
python3 tools/new_project_layout/migrate_project.py git set-branch \
--root /path/to/ApeEscapeRecomp --submodule psxrecomp --branch master
python3 tools/new_project_layout/migrate_project.py git update-submodules \
--root /path/to/ApeEscapeRecomp --remote
python3 tools/new_project_layout/migrate_project.py git commit \
--root /path/to/ApeEscapeRecomp -m "chore: bump submodules"
python3 tools/new_project_layout/migrate_project.py git push \
--root /path/to/ApeEscapeRecomp
python3 tools/new_project_layout/migrate_project.py git release \
--root /path/to/ApeEscapeRecomp --bump patch
```
Git ops shell out to `git` / `gh` (no force-push, amend, or hook skips). Submodule **branch** in `.gitmodules` is for `update --remote`; CI builds the committed **gitlink SHAs**.
### Ops (subset)
| Op | Purpose |
@@ -5,6 +5,7 @@ Examples:
python3 tools/new_project_layout/migrate_project.py audit --root ~/src/ApeEscapeRecomp
python3 tools/new_project_layout/migrate_project.py plan --root ~/src/ApeEscapeRecomp
python3 tools/new_project_layout/migrate_project.py apply --root ~/src/ApeEscapeRecomp --dry-run
python3 tools/new_project_layout/migrate_project.py git status --root ~/src/ApeEscapeRecomp
python3 tools/new_project_layout/migrate_project.py gui
"""
@@ -157,6 +157,168 @@ def cmd_gui(args: argparse.Namespace) -> int:
return run_gui(initial_root=root)
def _root_or_die(args: argparse.Namespace) -> Path | None:
root = Path(args.root).expanduser().resolve()
if not root.is_dir():
print(f"error: not a directory: {root}", file=sys.stderr)
return None
return root
def cmd_git_status(args: argparse.Namespace) -> int:
from project_studio.gitops import repo_status
root = _root_or_die(args)
if root is None:
return 2
st = repo_status(root)
if args.json:
print(json.dumps(st.to_dict(), indent=2))
return 0 if st.is_git else 1
print(f"Project Studio git v{__version__}")
print(f" root: {st.root}")
print(f" git: {st.is_git}")
if not st.is_git:
return 1
print(f" branch: {st.branch}" + (f" → {st.upstream}" if st.upstream else ""))
print(f" ahead/behind: {st.ahead}/{st.behind}")
print(f" dirty: {st.dirty} (staged={st.staged} unstaged={st.unstaged} untracked={st.untracked})")
print(f" origin: {st.remote_url or '(none)'}")
print(f" gh: {st.gh_repo or ('available' if st.gh_available else 'missing')}")
print()
print("Submodules:")
for s in st.submodules:
mark = "OK" if s.present else "MISSING"
print(
f" [{mark}] {s.path:<12} branch={s.branch or '-':<16} "
f"sha={s.sha or '-':<12} {s.url}"
)
if st.short_status:
print()
print("Status:")
print(st.short_status)
for n in st.notes:
print(f"note: {n}")
return 0
def cmd_git_ensure_submodules(args: argparse.Namespace) -> int:
from project_studio.gitops import ensure_known_submodules
root = _root_or_die(args)
if root is None:
return 2
results = ensure_known_submodules(
root,
psxrecomp_branch=args.psxrecomp_branch,
recomp_ui_branch=args.recomp_ui_branch,
dry_run=args.dry_run,
)
failed = 0
for r in results:
print(f" [{'OK' if r.ok else 'FAIL'}] {r.message}")
if r.detail:
print(f" {r.detail}")
if not r.ok:
failed += 1
return 1 if failed else 0
def cmd_git_set_branch(args: argparse.Namespace) -> int:
from project_studio.gitops import set_repo_branch, set_submodule_branch
root = _root_or_die(args)
if root is None:
return 2
if args.submodule:
r = set_submodule_branch(
root, args.submodule, args.branch, dry_run=args.dry_run
)
else:
r = set_repo_branch(
root, args.branch, create=args.create, dry_run=args.dry_run
)
print(f"[{'OK' if r.ok else 'FAIL'}] {r.message}")
if r.detail:
print(r.detail)
return 0 if r.ok else 1
def cmd_git_update_submodules(args: argparse.Namespace) -> int:
from project_studio.gitops import update_submodules
root = _root_or_die(args)
if root is None:
return 2
paths = [p.strip() for p in (args.paths or "").split(",") if p.strip()] or None
r = update_submodules(
root, paths=paths, remote=args.remote, dry_run=args.dry_run
)
print(f"[{'OK' if r.ok else 'FAIL'}] {r.message}")
if r.detail:
print(r.detail)
return 0 if r.ok else 1
def cmd_git_pull(args: argparse.Namespace) -> int:
from project_studio.gitops import pull
root = _root_or_die(args)
if root is None:
return 2
r = pull(root, dry_run=args.dry_run)
print(f"[{'OK' if r.ok else 'FAIL'}] {r.message}")
if r.detail:
print(r.detail)
return 0 if r.ok else 1
def cmd_git_commit(args: argparse.Namespace) -> int:
from project_studio.gitops import commit_all
root = _root_or_die(args)
if root is None:
return 2
r = commit_all(root, args.message, dry_run=args.dry_run)
print(f"[{'OK' if r.ok else 'FAIL'}] {r.message}")
if r.detail:
print(r.detail)
return 0 if r.ok else 1
def cmd_git_push(args: argparse.Namespace) -> int:
from project_studio.gitops import push
root = _root_or_die(args)
if root is None:
return 2
r = push(root, dry_run=args.dry_run)
print(f"[{'OK' if r.ok else 'FAIL'}] {r.message}")
if r.detail:
print(r.detail)
return 0 if r.ok else 1
def cmd_git_release(args: argparse.Namespace) -> int:
from project_studio.gitops import run_release_workflow
root = _root_or_die(args)
if root is None:
return 2
r = run_release_workflow(
root,
version=args.version or "",
bump=args.bump,
publish=not args.no_publish,
reuse_cached_emitters=not args.no_reuse_cached_emitters,
dry_run=args.dry_run,
)
print(f"[{'OK' if r.ok else 'FAIL'}] {r.message}")
if r.detail:
print(r.detail)
return 0 if r.ok else 1
def build_parser() -> argparse.ArgumentParser:
ap = argparse.ArgumentParser(
prog="migrate_project",
@@ -223,6 +385,75 @@ def build_parser() -> argparse.ArgumentParser:
p_gui.add_argument("--root", default=None, help="Optional initial game root")
p_gui.set_defaults(func=cmd_gui)
# --- git / GitHub ---
p_git = sub.add_parser("git", help="Git / GitHub ops on a game repo")
git_sub = p_git.add_subparsers(dest="git_cmd", required=True)
def add_git_root(p: argparse.ArgumentParser) -> None:
p.add_argument("--root", required=True, help="Game repository root")
p.add_argument("--dry-run", action="store_true")
p_gs = git_sub.add_parser("status", help="Repo + submodule status")
add_git_root(p_gs)
p_gs.add_argument("--json", action="store_true")
p_gs.set_defaults(func=cmd_git_status)
p_ge = git_sub.add_parser("ensure-submodules", help="Add psxrecomp + recomp-ui")
add_git_root(p_ge)
p_ge.add_argument("--psxrecomp-branch", default="master")
p_ge.add_argument("--recomp-ui-branch", default="master")
p_ge.set_defaults(func=cmd_git_ensure_submodules)
p_gb = git_sub.add_parser(
"set-branch",
help="Set game branch (checkout) or submodule tracking branch",
)
add_git_root(p_gb)
p_gb.add_argument("--branch", required=True)
p_gb.add_argument(
"--submodule",
help="Submodule path (e.g. psxrecomp). Omit to checkout game branch.",
)
p_gb.add_argument(
"--create",
action="store_true",
help="Create/reset game branch (-B) when not using --submodule",
)
p_gb.set_defaults(func=cmd_git_set_branch)
p_gu = git_sub.add_parser("update-submodules", help="git submodule update")
add_git_root(p_gu)
p_gu.add_argument(
"--remote",
action="store_true",
help="Update to remote tracking branch tip (then commit gitlinks)",
)
p_gu.add_argument("--paths", help="Comma-separated submodule paths")
p_gu.set_defaults(func=cmd_git_update_submodules)
p_gpull = git_sub.add_parser("pull", help="git pull --ff-only")
add_git_root(p_gpull)
p_gpull.set_defaults(func=cmd_git_pull)
p_gc = git_sub.add_parser("commit", help="git add -A && git commit")
add_git_root(p_gc)
p_gc.add_argument("-m", "--message", required=True)
p_gc.set_defaults(func=cmd_git_commit)
p_gpush = git_sub.add_parser("push", help="git push -u origin HEAD")
add_git_root(p_gpush)
p_gpush.set_defaults(func=cmd_git_push)
p_gr = git_sub.add_parser("release", help="gh workflow run release.yml")
add_git_root(p_gr)
p_gr.add_argument("--version", default="", help="Empty = auto-bump")
p_gr.add_argument(
"--bump", choices=("patch", "minor", "major"), default="patch"
)
p_gr.add_argument("--no-publish", action="store_true")
p_gr.add_argument("--no-reuse-cached-emitters", action="store_true")
p_gr.set_defaults(func=cmd_git_release)
return ap
File diff suppressed because it is too large Load Diff
@@ -1,5 +1,8 @@
#!/usr/bin/env python3
"""Launch the Project Studio GUI."""
"""Launch the Project Studio GUI (CustomTkinter).
First run auto-bootstraps ``.venv`` from ``requirements-gui.txt``.
"""
from __future__ import annotations