import os import subprocess from pathlib import Path import sys import shutil import time # ========== CONFIG ========== SRC_DIRS = ["src/src", "src/vendor"] INCLUDE_DIRS = [ "src/include", "src/vendor", "src/vendor/imgui", "C:/msys64/mingw64/include" ] LIB_DIRS = ["C:/msys64/mingw64/lib"] BUILD_DIR = Path("src/build") TARGET = BUILD_DIR / "app.exe" LOG_FILE = Path("build.log") LIBS = ["glfw3", "glew32", "opengl32", "gdi32", "yaml-cpp", "comdlg32", "ssl", "crypto"] CXX = "g++" CXXFLAGS = ["-std=c++20", "-Wall"] + [f"-I{inc}" for inc in INCLUDE_DIRS] LDFLAGS = [f"-L{lib}" for lib in LIB_DIRS] + [f"-l{lib}" for lib in LIBS] # ========== COLOR UTILS ========== class Colors: HEADER = '\033[95m' OKBLUE = '\033[94m' OKCYAN = '\033[96m' OKGREEN = '\033[92m' WARNING = '\033[93m' FAIL = '\033[91m' BOLD = '\033[1m' RESET = '\033[0m' GRAY = '\033[90m' def color(text, style): return f"{style}{text}{Colors.RESET}" def banner(title): print(color(f"\n╔═ {title} ═══════════════════════════════╗", Colors.BOLD + Colors.OKBLUE)) def info(msg): print(color(f"✅ {msg}", Colors.OKGREEN)) def warn(msg): print(color(f"⚠️ {msg}", Colors.WARNING)) def error(msg): print(color(f"❌ {msg}", Colors.FAIL)) def log(msg: str): with LOG_FILE.open("a", encoding="utf-8") as f: f.write(msg + "\n") # ========== BUILD SYSTEM ========== def find_cpp_files(): cpp_files = [] for folder in SRC_DIRS: for path in Path(folder).rglob("*.cpp"): cpp_files.append(path) return cpp_files def obj_path(source): return BUILD_DIR / source.relative_to("src").with_suffix(".o") def dep_path(obj): return obj.with_suffix(".d") def parse_dep_file(dep_file): if not dep_file.exists(): return [] deps = [] with dep_file.open() as f: for line in f: line = line.strip().replace("\\", "") if ":" in line: line = line.split(":", 1)[1] deps.extend(line.strip().split()) return deps def compile_source(source, obj): obj.parent.mkdir(parents=True, exist_ok=True) cmd = [CXX, *CXXFLAGS, "-MMD", "-MP", "-c", str(source), "-o", str(obj)] try: print(f"{color('🔨 Compiling:', Colors.OKCYAN)} {source}") result = subprocess.run(cmd, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) log(f"[COMPILE] {' '.join(cmd)}{result.stdout.decode()}{result.stderr.decode()}") except subprocess.CalledProcessError as e: error(f"Failed to compile {source}") print("🔧 Command:", " ".join(cmd)) print(e.stderr.decode()) log(f"[ERROR] {' '.join(cmd)}\n{e.stderr.decode()}") sys.exit(1) def link_objects(obj_files): cmd = [CXX, *map(str, obj_files), "-o", str(TARGET), *LDFLAGS] try: print(f"{color('📦 Linking:', Colors.OKBLUE)} {TARGET}") result = subprocess.run(cmd, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) log(f"[LINK] {' '.join(cmd)}\n{result.stdout.decode()}{result.stderr.decode()}") except subprocess.CalledProcessError as e: error("Linking failed.") print("🔧 Command:", " ".join(cmd)) print(e.stderr.decode()) log(f"[ERROR] {' '.join(cmd)}\n{e.stderr.decode()}") sys.exit(1) def build(): build_start = time.time() banner("🚀 Building Project") cpp_files = find_cpp_files() obj_files = [] for source in cpp_files: obj = obj_path(source) dep = dep_path(obj) obj_mtime = obj.stat().st_mtime if obj.exists() else 0 needs_build = not obj.exists() # If source is newer than object if not needs_build and source.stat().st_mtime > obj_mtime: needs_build = True # If any dependencies are newer than object if not needs_build and dep.exists(): for dep_file in parse_dep_file(dep): try: if Path(dep_file).exists() and Path(dep_file).stat().st_mtime > obj_mtime: needs_build = True break except Exception: needs_build = True break if needs_build: compile_source(source, obj) else: print(f"{color('👌 Up-to-date:', Colors.GRAY)} {source}") obj_files.append(obj) link_objects(obj_files) banner("✅ Build Complete") build_end = time.time() log(f"[TIME] Build duration: {build_end - build_start:.2f}s") print(color(f"⏱ Build time: {build_end - build_start:.2f}s", Colors.OKCYAN)) def run(): build() if TARGET.exists(): banner("🚀 Running") try: result = subprocess.run(str(TARGET), check=True) log("[RUN] Executed app.exe successfully.") except subprocess.CalledProcessError as e: error("Program exited with error.") log(f"[ERROR] Runtime crash\n{e}") sys.exit(e.returncode) else: error("Executable not found.") log("[ERROR] Executable not found.") def clean(): banner("🧹 Cleaning") if BUILD_DIR.exists(): shutil.rmtree(BUILD_DIR) info("Build directory removed.") log("[CLEAN] Build directory removed.") else: warn("Nothing to clean.") log("[CLEAN] No build directory found.") if LOG_FILE.exists(): LOG_FILE.unlink() info("Log file cleared.") # ========== ENTRY ========== if __name__ == "__main__": total_start = time.time() # Clear old log LOG_FILE.write_text("", encoding="utf-8") try: if "clean" in sys.argv: clean() elif "run" in sys.argv: run() else: build() except KeyboardInterrupt: error("Interrupted by user.") log("[ERROR] Build interrupted by user.") sys.exit(1) total_end = time.time() print(color(f"\n⏱ Total time: {total_end - total_start:.2f}s", Colors.BOLD + Colors.OKGREEN)) log(f"[TIME] Total runtime: {total_end - total_start:.2f}s")