Files
sophia/bin/dev
T
2026-10-04 01:03:47 -07:00

212 lines
7.7 KiB
Python
Executable File

#!/usr/bin/env python3
import json
import os
import shlex
import shutil
import subprocess
import sys
from helpers.gitea import GiteaCommand
from helpers.renderdoc import RenderDocCommand
from helpers.shaders import ShadersCommand
from helpers.tracy import TracyCommand
COMMAND_TO_METHOD = {
"branch-from": "branch_from",
"bash-completions": "bash_completions",
}
METHOD_TO_COMMAND = {method: command for command, method in COMMAND_TO_METHOD.items()}
WAIT_FOR_PROCESS = "wait_for_process"
class DevCommand(GiteaCommand, RenderDocCommand, ShadersCommand, TracyCommand):
PROJECT_NAME = "sophia"
DEFAULT_PRESET = "debug"
def __init__(self):
self.last_exit_status = 0
# Core logic
def call(self, *args, **kwargs):
if not args:
print("Usage: dev <command> [args...]\n\nCommands:")
for command in self._commands():
print(f" {command}")
return
command, rest = args[0], args[1:]
method_name = COMMAND_TO_METHOD.get(command, command)
method = getattr(self, method_name, None)
if callable(method) and not method_name.startswith("_") and method_name != "call":
return method(*rest, **kwargs)
# Primary command wrappers
##
# Configures and builds a CMake preset (see CMakePresets.json).
# dev build [preset] [--fresh]
# The preset defaults to debug and builds into build/<preset>. --fresh
# discards the preset's CMake cache before configuring.
def build(self, *args, **kwargs):
preset, fresh = self._parse_preset_args(args, "build")
self._build_preset(preset, fresh)
##
# Builds the debug preset (incrementally), then runs the editor from
# build/debug. Any arguments are passed through to the editor.
# dev editor [args...]
def editor(self, *args, **kwargs):
preset = self.DEFAULT_PRESET
self._build_preset(preset, fresh=False)
preset_dir = os.path.join(self._project_root(), "build", preset)
binary = os.path.join(preset_dir, "bin", "editor")
print(f"Running: {shlex.join([binary, *args])}")
os.chdir(preset_dir)
os.execv(binary, [binary, *args])
##
# Builds the debug preset (incrementally), then runs the testbed from
# build/debug. Any arguments are passed through to the testbed.
# dev testbed [args...]
def testbed(self, *args, **kwargs):
preset = self.DEFAULT_PRESET
self._build_preset(preset, fresh=False)
preset_dir = os.path.join(self._project_root(), "build", preset)
binary = os.path.join(preset_dir, "bin", "testbed")
print(f"Running: {shlex.join([binary, *args])}")
os.chdir(preset_dir)
os.execv(binary, [binary, *args])
##
# Builds the debug preset (incrementally), then runs the raw Vulkan
# sandbox from build/debug. Any arguments are passed through.
# dev sandbox [args...]
def sandbox(self, *args, **kwargs):
preset = self.DEFAULT_PRESET
self._build_preset(preset, fresh=False)
preset_dir = os.path.join(self._project_root(), "build", preset)
binary = os.path.join(preset_dir, "bin", "sandbox")
print(f"Running: {shlex.join([binary, *args])}")
os.chdir(preset_dir)
os.execv(binary, [binary, *args])
# Custom helpers
def ownit(self, *args, **kwargs):
file_or_directory = args[0] if args else None
if file_or_directory is None:
raise ValueError("Must provide a file or directory path.")
if sys.platform.startswith("linux"):
print(f"Take ownership of the file or directory? {file_or_directory}")
command = f"sudo chown -R {self._user_id()}:{self._group_id()} {file_or_directory}"
self._exec(command, replace=True)
else:
raise NotImplementedError(f"Not implemented for platform {sys.platform}")
def bash_completions(self, *args, **kwargs):
print(" ".join(self._commands()))
# Private helpers
def _commands(self):
return sorted(
METHOD_TO_COMMAND.get(name, name)
for name in dir(DevCommand)
if not name.startswith("_") and name != "call" and callable(getattr(DevCommand, name))
)
def _parse_preset_args(self, args, command):
preset = None
fresh = False
for arg in args:
if arg == "--fresh":
fresh = True
elif arg.startswith("-"):
raise ValueError(f"Unknown option: {arg}")
elif preset is None:
preset = arg
else:
raise ValueError(f"Usage: dev {command} [preset] [--fresh]")
preset = preset or self.DEFAULT_PRESET
presets = self._configure_presets()
if preset not in presets:
raise ValueError(f"Unknown preset: {preset} (available: {', '.join(presets)})")
return preset, fresh
def _configure_presets(self):
with open(os.path.join(self._project_root(), "CMakePresets.json")) as f:
presets = json.load(f)["configurePresets"]
return [preset["name"] for preset in presets if not preset.get("hidden")]
def _build_preset(self, preset, fresh):
configure = ["cmake", "--preset", preset]
if fresh:
configure.append("--fresh")
self._run_in_root(configure)
self._copy_compile_commands(preset)
self._run_in_root(["cmake", "--build", "--preset", preset])
# Editor tooling reads build/compile_commands.json, so point it at the
# most recently configured preset.
def _copy_compile_commands(self, preset):
build_dir = os.path.join(self._project_root(), "build")
source = os.path.join(build_dir, preset, "compile_commands.json")
if os.path.isfile(source):
shutil.copyfile(source, os.path.join(build_dir, "compile_commands.json"))
def _run_in_root(self, command):
print(f"Running: {shlex.join(command)}")
result = subprocess.run(command, cwd=self._project_root())
if result.returncode != 0:
sys.exit(result.returncode)
def _wait_for_process_with_logging(self, command):
process = self._exec(command, capture=True)
for line in process.stdout:
print(line, end="")
process.wait()
self.last_exit_status = process.returncode
def _exec_in_root(self, command, **kwargs):
return self._exec(f"cd {self._project_root()} && {command}", **kwargs)
def _exec(self, command, *, replace=False, capture=False):
print(f"Running: {command}")
if replace:
os.execv("/bin/sh", ["/bin/sh", "-c", command])
if capture:
return subprocess.Popen(
["/bin/sh", "-c", f"{command} 2>&1"],
stdout=subprocess.PIPE,
text=True,
)
return subprocess.run(["/bin/sh", "-c", command])
def _project_root(self):
return os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
def _take_over_needed(self, file_or_directory):
result = subprocess.run(
["/bin/sh", "-c", f"find {file_or_directory} -not -user {self._user_id()} -print -quit | grep -q ."],
)
return result.returncode == 0
def _user_id(self):
if not sys.platform.startswith("linux"):
raise NotImplementedError(f"Not implemented for platform {sys.platform}")
return subprocess.run(["id", "-u"], stdout=subprocess.PIPE, text=True).stdout.strip()
def _group_id(self):
if not sys.platform.startswith("linux"):
raise NotImplementedError(f"Not implemented for platform {sys.platform}")
return subprocess.run(["id", "-g"], stdout=subprocess.PIPE, text=True).stdout.strip()
if __name__ == "__main__":
DevCommand().call(*sys.argv[1:])