219 lines
8.0 KiB
Python
Executable File
219 lines
8.0 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
|
|
|
|
##
|
|
# Counts lines of code from the project root, skipping paths listed in
|
|
# .clocignore. Any arguments are passed through to cloc.
|
|
# dev cloc [args...]
|
|
def cloc(self, *args, **kwargs):
|
|
self._run_in_root(["cloc", "--exclude-list-file=.clocignore", *args, "."])
|
|
|
|
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:])
|