From b0be7f0c0ba519698f5603ebf233335f225d668b Mon Sep 17 00:00:00 2001 From: Kyler Date: Thu, 27 Nov 2025 22:53:28 -0700 Subject: [PATCH] Created a python build script --- SLS_C/build.py | 207 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 207 insertions(+) create mode 100644 SLS_C/build.py diff --git a/SLS_C/build.py b/SLS_C/build.py new file mode 100644 index 0000000..20e477a --- /dev/null +++ b/SLS_C/build.py @@ -0,0 +1,207 @@ +#!/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() + + +# --------------------------------------------------------------------- +# GIT COMMIT HASH +# --------------------------------------------------------------------- +def git_commit_hash(): + try: + result = subprocess.check_output( + ["git", "describe", "--always", "--dirty", "--abbrev=7"], + cwd=".", + stderr=subprocess.DEVNULL, + text=True + ) + return result.strip() + 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 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(["python3", 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()