From 659d6010e3ba24fdaca4d4ccf993f84b9251cd5f Mon Sep 17 00:00:00 2001 From: Caleb Burke Date: Sun, 6 Sep 2026 22:19:32 -0700 Subject: [PATCH] dev command helper --- bin/README.md | 3 + bin/dev | 188 ++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 191 insertions(+) create mode 100644 bin/README.md create mode 100755 bin/dev diff --git a/bin/README.md b/bin/README.md new file mode 100644 index 0000000..f28f01a --- /dev/null +++ b/bin/README.md @@ -0,0 +1,3 @@ +# bin/ + +Development helper scripts diff --git a/bin/dev b/bin/dev new file mode 100755 index 0000000..64f5856 --- /dev/null +++ b/bin/dev @@ -0,0 +1,188 @@ +#!/usr/bin/env python3 + +import os +import re +import shutil +import struct +import subprocess +import sys + +COMMAND_TO_METHOD = { + "bash-completions": "bash_completions", +} +METHOD_TO_COMMAND = {method: command for command, method in COMMAND_TO_METHOD.items()} + +WAIT_FOR_PROCESS = "wait_for_process" + +class DevHelper: + SHADER_EXTENSIONS = (".vert", ".frag", ".comp", ".geom", ".tesc", ".tese") + + def __init__(self): + self.last_exit_status = 0 + + # Core logic + def call(self, *args, **kwargs): + if not args: + 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 + def build(self, *args, **kwargs): + command = f"rm -rf build && cmake -S . -B build -DCMAKE_BUILD_TYPE=Debug" + self._exec_in_root(command, replace=True) + + def shaders(self, *args, **kwargs): + shaders_dir = os.path.join(self._project_root(), "engine", "src", "shaders") + if not os.path.isdir(shaders_dir): + raise ValueError(f"Shaders directory not found: {shaders_dir}") + + glslc = self._find_glslc() + + shader_paths = sorted( + os.path.join(root, filename) + for root, _dirs, files in os.walk(shaders_dir) + for filename in files + if filename.endswith(self.SHADER_EXTENSIONS) + ) + + if not shader_paths: + print(f"No shaders found in {shaders_dir}") + return + + for shader_path in shader_paths: + self._generate_shader_header(glslc, shader_path) + + # 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): + completions = sorted( + METHOD_TO_COMMAND.get(name, name) + for name in vars(DevHelper) + if not name.startswith("_") and name != "call" and callable(getattr(DevHelper, name)) + ) + print(" ".join(completions)) + + # Private helpers + def _find_glslc(self): + glslc = shutil.which("glslc") + if glslc is None: + raise RuntimeError( + "glslc not found on PATH (install the Vulkan SDK)" + ) + return glslc + + def _generate_shader_header(self, glslc, shader_path): + relative_path = os.path.relpath(shader_path, self._project_root()) + print(f"Compiling shader: {relative_path}") + + result = subprocess.run( + [glslc, shader_path, "-o", "-"], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + if result.returncode != 0: + raise RuntimeError( + f"failed to compile {shader_path}: {result.stderr.decode()}" + ) + + spirv = result.stdout + if len(spirv) % 4 != 0: + raise RuntimeError( + f"invalid SPIR-V output for {shader_path} (size not a multiple of 4)" + ) + + word_count = len(spirv) // 4 + words = struct.unpack(f"<{word_count}I", spirv) + + filename = os.path.basename(shader_path) + variable_name = f"{filename.replace('.', '_')}_spv" + header_path = shader_path + ".spv.h" + + body_lines = [ + " " + ", ".join(f"0x{word:08x}" for word in words[i:i + 4]) + "," + for i in range(0, len(words), 4) + ] + + header = "\n".join([ + "#pragma once", + "", + "#include ", + "#include ", + "", + "namespace sophia::shaders {", + "", + f"inline constexpr std::array {variable_name} = {{", + *body_lines, + "};", + "", + "} // namespace sophia::shaders", + "", + ]) + + with open(header_path, "w") as f: + f.write(header) + + print(f"Wrote {os.path.relpath(header_path, self._project_root())} " + f"({word_count * 4} bytes)") + + 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__": + DevHelper().call(*sys.argv[1:])