import os import subprocess from pathlib import Path import sys import shutil import time import json from remake_config import * # ========== 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") # ========== CACHE ========== def load_cache(): try: if CACHE_FILE.exists(): return json.loads(CACHE_FILE.read_text()) except json.decoder.JSONDecodeError: error("Failed to Read Cache File.") return {} def save_cache(data): CACHE_FILE.write_text(json.dumps(data, indent=2)) # ========== PACKAGE DISCOVERY ========== class AutoLib: def __init__(self, name): self.name = name self.path = None def find(self, search_paths, cache): if self.name in cache: self.path = Path(cache[self.name]) return self.path.exists() for path in search_paths: for root, _, files in os.walk(path): for ext in [".lib", ".a"]: fname = f"lib{self.name}{ext}" if fname in files: self.path = Path(root) / fname cache[self.name] = str(self.path) info(f"Found {self.name} at {self.path}") return True return False class AutoInclude: def __init__(self, name): self.name = name self.path = None def find(self, search_paths, cache): if self.name in cache: self.path = Path(cache[self.name]) return self.path.exists() for path in search_paths: for root, _, files in os.walk(path): if f"{self.name}.h" in files or Path(root).name == self.name: self.path = Path(root) cache[self.name] = str(self.path) info(f"Found header {self.name} at {self.path}") return True return False def resolve_packages(): cache = load_cache() extra_link_paths, resolved_libs, extra_includes = [], [], [] for name in AUTO_LIBS: lib = AutoLib(name) if lib.find(LIB_DIRS, cache): extra_link_paths.append(str(lib.path.parent)) resolved_libs.append(f"-l{lib.name}") else: error(f"Library {lib.name} not found.") sys.exit(1) for name in AUTO_INCLUDES: inc = AutoInclude(name) if inc.find(INCLUDE_DIRS, cache): extra_includes.append(str(inc.path)) save_cache(cache) return list(set(extra_link_paths)), resolved_libs, list(set(extra_includes)) # ========== BUILD SYSTEM ========== def find_cpp_files(): extensions = [".c", ".cpp"] files = [] for folder in SRC_DIRS: for ext in extensions: for path in Path(folder).rglob(f"*{ext}"): files.append(path) return files def obj_path(source): source_abs = source.resolve() for src_root in SRC_DIRS: try: base = Path(src_root).resolve() rel = source_abs.relative_to(base) return BUILD_DIR / rel.with_suffix(".o") except ValueError: continue # fallback: flatten path return BUILD_DIR / source.name.replace("/", "_").replace("\\", "_") + ".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, includes): obj.parent.mkdir(parents=True, exist_ok=True) is_cpp = source.suffix == ".cpp" compiler = CXX if is_cpp else CC flags = CXXFLAGS if is_cpp else CFLAGS cmd = [compiler, *flags, *[f"-I{inc}" for inc in includes], "-MMD", "-MP", "-c", str(source), "-o", str(obj)] try: print(f"{color('🔨 Compiling:', Colors.OKCYAN)} {source}") subprocess.run(cmd, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) log(f"[COMPILE] {' '.join(cmd)}") 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, link_dirs, libs): cmd = [CXX, *map(str, obj_files), "-o", str(TARGET), *[f"-L{p}" for p in link_dirs], *libs] try: print(f"{color('📦 Linking:', Colors.OKBLUE)} {TARGET}") subprocess.run(cmd, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) log(f"[LINK] {' '.join(cmd)}") 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 = [] link_dirs, libs, extra_includes = resolve_packages() all_includes = INCLUDE_DIRS + extra_includes 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() or source.stat().st_mtime > obj_mtime 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, all_includes) else: print(f"{color('👌 Up-to-date:', Colors.GRAY)} {source}") obj_files.append(obj) link_objects(obj_files, link_dirs, libs) banner("✅ Build Complete") print(color(f"⏱ Build time: {time.time() - build_start:.2f}s", Colors.OKCYAN)) def run(): build() if TARGET.exists(): banner("🚀 Running") try: 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.") def clean(): banner("🧹 Cleaning") if BUILD_DIR.exists(): shutil.rmtree(BUILD_DIR) info("Build directory removed.") if LOG_FILE.exists(): LOG_FILE.unlink() info("Log file cleared.") if CACHE_FILE.exists(): CACHE_FILE.unlink() info("Cache file cleared.") def ensure_directories(): BUILD_DIR.mkdir(parents=True, exist_ok=True) LOG_FILE.parent.mkdir(parents=True, exist_ok=True) CACHE_FILE.parent.mkdir(parents=True, exist_ok=True) TARGET.parent.mkdir(parents=True, exist_ok=True) # <- ensure this! for dir in SRC_DIRS: Path(dir).mkdir(parents=True, exist_ok=True) for inc in INCLUDE_DIRS: Path(inc).mkdir(parents=True, exist_ok=True) for lib in LIB_DIRS: Path(lib).mkdir(parents=True, exist_ok=True) # ========== ENTRY ========== if __name__ == "__main__": start = time.time() LOG_FILE.write_text("", encoding="utf-8") ensure_directories() try: if "clean" in sys.argv: clean() elif "run" in sys.argv: run() else: build() except KeyboardInterrupt: error("Interrupted by user.") log("[ERROR] Interrupted by user.") sys.exit(1) print(color(f"\n⏱ Total time: {time.time() - start:.2f}s", Colors.BOLD + Colors.OKGREEN))