#!/usr/bin/env python3 import os import sys import subprocess from pathlib import Path import shutil # --------------------------------------------------------------------- # CONFIG # --------------------------------------------------------------------- SRC_DIR = Path("src") TEST_DIR = Path("tests") OBJ_DIR = Path("obj") BIN_DIR = Path("bin") TARGET = BIN_DIR / "sls" TEST_TARGET = BIN_DIR / "sls_tests" # Unix gcc/clang flags COMMON_FLAGS = ["-std=c99", "-Wall", "-Wextra", "-Werror", "-Iinclude", "-g"] TEST_FLAGS = ["-std=c99", "-Wall", "-Wextra", "-Wno-unused-function", "-Werror", "-Iinclude", "-g", "-O0"] # Windows MSVC flags MSVC_FLAGS = ["/std:c11", "/Zi", "/Iinclude"] MSVC_TEST_FLAGS = MSVC_FLAGS + [] # --------------------------------------------------------------------- # COMPILER DETECTION # --------------------------------------------------------------------- def detect_compiler(): if os.name == "nt": return ("cl", "msvc") return ("gcc", "gcc") CC, CC_KIND = detect_compiler() # --------------------------------------------------------------------- # PYTHON DETECTION # --------------------------------------------------------------------- def detect_python(): if os.name == "nt": return "python" return "python3" PYTHON = detect_python() # --------------------------------------------------------------------- # GIT COMMIT HASH # --------------------------------------------------------------------- def git_commit_hash(): try: result_hash = subprocess.check_output( ["git", "describe", "--always", "--dirty", "--abbrev=7"], cwd=".", stderr=subprocess.DEVNULL, text=True ).strip() result_date = subprocess.check_output( ["git", "show", "-s", "--format=%ci"], cwd=".", stderr=subprocess.DEVNULL, text=True ).strip() return f"{result_hash} {result_date}" except Exception: return "unknown" GIT_HASH = git_commit_hash() # --------------------------------------------------------------------- # UTILS # --------------------------------------------------------------------- def mkdir(p: Path): p.mkdir(parents=True, exist_ok=True) def run(cmd): print(">>", " ".join(cmd)) subprocess.check_call(cmd) def is_up_to_date(src, obj, dep): if "meta" in [src.stem, obj.stem, dep.stem]: return False if not obj.exists(): return False if dep.exists() and dep.stat().st_mtime > obj.stat().st_mtime: return False return src.stat().st_mtime < obj.stat().st_mtime # --------------------------------------------------------------------- # BUILD RULES # --------------------------------------------------------------------- def compile_source(src: Path, is_test=False): mkdir(OBJ_DIR) obj = OBJ_DIR / (src.stem + ".o") dep = OBJ_DIR / (src.stem + ".d") if is_up_to_date(src, obj, dep): return obj if CC_KIND == "msvc": flags = MSVC_TEST_FLAGS if is_test else MSVC_FLAGS cmd = [CC] + flags + ["/Fo" + str(obj), "/c", str(src), f"/DGIT_COMMIT_HASH=\"{GIT_HASH}\""] else: flags = TEST_FLAGS if is_test else COMMON_FLAGS cmd = [CC] + flags + [ f"-DGIT_COMMIT_HASH=\"{GIT_HASH}\"", "-MMD", "-MP", "-c", str(src), "-o", str(obj) ] run(cmd) return obj def link_executable(objects, output: Path): mkdir(BIN_DIR) if CC_KIND == "msvc": cmd = [CC] + list(map(str, objects)) + ["/Fe" + str(output)] else: cmd = [CC] + list(map(str, objects)) + ["-lm", "-o", str(output)] run(cmd) # --------------------------------------------------------------------- # TEST CASE GENERATION # --------------------------------------------------------------------- def generate_tests(): script = Path("../SLS_Tests/yaml_to_c_tests.py") yaml = Path("../SLS_Tests/cases.yaml") out = TEST_DIR / "lexer_tests.c" if not script.exists() or not yaml.exists(): print("Test generation skipped (missing files).") return False if out.exists() and out.stat().st_mtime > yaml.stat().st_mtime: return True run([PYTHON, str(script), str(yaml), str(out)]) return True # --------------------------------------------------------------------- # TARGETS # --------------------------------------------------------------------- def build_main(): sources = list(SRC_DIR.glob("*.c")) objects = [compile_source(s, is_test=False) for s in sources] link_executable(objects, TARGET) def build_tests(): generate_tests() test_sources = list(TEST_DIR.glob("*.c")) main_sources = [s for s in SRC_DIR.glob("*.c") if s.name != "main.c"] test_objects = [compile_source(s, is_test=True) for s in test_sources] shared_objects = [compile_source(s, is_test=False) for s in main_sources] link_executable(test_objects + shared_objects, TEST_TARGET) def clean(): shutil.rmtree(OBJ_DIR, ignore_errors=True) shutil.rmtree(BIN_DIR, ignore_errors=True) print("Cleaned.") def run_main(): build_main() print("\n--- Running program ---\n") subprocess.call([str(TARGET)]) def run_tests(): build_tests() print("\n--- Running tests ---\n") subprocess.call([str(TEST_TARGET)]) def debug_tests(): build_tests() if os.name == "nt": print("Debugging on Windows requires Visual Studio debugger.") else: subprocess.call(["gdb", str(TEST_TARGET)]) # --------------------------------------------------------------------- # ENTRY POINT # --------------------------------------------------------------------- def main(): if len(sys.argv) < 2: print("Usage: python3 build.py [all|build|test|run|debug|clean]") return cmd = sys.argv[1] match cmd: case "all" | "main": build_main() case "build": build_main() case "run": run_main() case "test": run_tests() case "debug": debug_tests() case "clean": clean() case _: print(f"Unknown target: {cmd}") if __name__ == "__main__": main()