import argparse import json import os import shutil import subprocess import sys from pathlib import Path from typing import Tuple from ci.paths import PROJECT_ROOT from ci.running_process import RunningProcess BUILD_DIR = PROJECT_ROOT / "tests" / ".build" BUILD_DIR.mkdir(parents=True, exist_ok=True) def clean_build_directory(): print("Cleaning build directory...") shutil.rmtree(BUILD_DIR, ignore_errors=True) BUILD_DIR.mkdir(parents=True, exist_ok=True) print("Build directory cleaned.") HERE = Path(__file__).resolve().parent WASM_BUILD = False USE_ZIG = False USE_CLANG = False def _has_system_clang_compiler() -> bool: CLANG = shutil.which("clang") CLANGPP = shutil.which("clang++") LLVM_AR = shutil.which("llvm-ar") return CLANG is not None and CLANGPP is not None and LLVM_AR is not None def use_clang_compiler() -> Tuple[Path, Path, Path]: assert _has_system_clang_compiler(), "Clang system compiler not found" CLANG = shutil.which("clang") CLANGPP = shutil.which("clang++") LLVM_AR = shutil.which("llvm-ar") assert CLANG is not None, "clang compiler not found" assert CLANGPP is not None, "clang++ compiler not found" assert LLVM_AR is not None, "llvm-ar not found" # Set environment variables for C and C++ compilers os.environ["CC"] = CLANG os.environ["CXX"] = CLANGPP os.environ["AR"] = LLVM_AR os.environ["CXXFLAGS"] = os.environ.get("CXXFLAGS", "") + " -ferror-limit=1" os.environ["CFLAGS"] = os.environ.get("CFLAGS", "") + " -ferror-limit=1" if WASM_BUILD: wasm_flags = [ "--target=wasm32", "-O3", "-flto", # "-nostdlib", # "-Wl,--no-entry", # "-Wl,--export-all", # "-Wl,--lto-O3", # "-Wl,-z,stack-size=8388608", # 8 * 1024 * 1024 (8MiB) ] os.environ["CFLAGS"] = " ".join(wasm_flags) os.environ["CXXFLAGS"] = " ".join(wasm_flags) print(f"CC: {CLANG}") print(f"CXX: {CLANGPP}") print(f"AR: {LLVM_AR}") return Path(CLANG), Path(CLANGPP), Path(LLVM_AR) def use_zig_compiler() -> Tuple[Path, Path, Path]: assert 0 == os.system( "uv run python -m ziglang version" ), "Zig-clang compiler not found" uv_path_str: str | None = shutil.which("uv") assert uv_path_str is not None, "uv not found in PATH" uv_path = Path(uv_path_str).resolve() zig_command = f'"{uv_path}" run python -m ziglang' # We are going to build up shell scripts that look like cc, c++, and ar. It will contain the actual build command. CC_PATH = BUILD_DIR / "cc" CXX_PATH = BUILD_DIR / "c++" AR_PATH = BUILD_DIR / "ar" if sys.platform == "win32": CC_PATH = CC_PATH.with_suffix(".cmd") CXX_PATH = CXX_PATH.with_suffix(".cmd") AR_PATH = AR_PATH.with_suffix(".cmd") CC_PATH.write_text(f"@echo off\n{zig_command} cc %* 2>&1\n") CXX_PATH.write_text(f"@echo off\n{zig_command} c++ %* 2>&1\n") AR_PATH.write_text(f"@echo off\n{zig_command} ar %* 2>&1\n") else: cc_cmd = f'#!/bin/bash\n{zig_command} cc "$@"\n' cxx_cmd = f'#!/bin/bash\n{zig_command} c++ "$@"\n' ar_cmd = f'#!/bin/bash\n{zig_command} ar "$@"\n' CC_PATH.write_text(cc_cmd) CXX_PATH.write_text(cxx_cmd) AR_PATH.write_text(ar_cmd) CC_PATH.chmod(0o755) CXX_PATH.chmod(0o755) AR_PATH.chmod(0o755) # if WASM_BUILD: # wasm_flags = [ # # "--target=wasm32", # # "-O3", # # "-flto", # # "-nostdlib", # "-Wl,--no-entry", # # "-Wl,--export-all", # # "-Wl,--lto-O3", # "-Wl,-z,stack-size=8388608", # 8 * 1024 * 1024 (8MiB) # ] # os.environ["CFLAGS"] = " ".join(wasm_flags) # os.environ["CXXFLAGS"] = " ".join(wasm_flags) cc, cxx = CC_PATH, CXX_PATH # use the system path, so on windows this looks like "C:\Program Files\Zig\zig.exe" cc_path: Path | str = cc.resolve() cxx_path: Path | str = cxx.resolve() if sys.platform == "win32": cc_path = str(cc_path).replace("/", "\\") cxx_path = str(cxx_path).replace("/", "\\") # print out the paths print(f"CC: {cc_path}") print(f"CXX: {cxx_path}") print(f"AR: {AR_PATH}") # sys.exit(1) # Set environment variables for C and C++ compilers os.environ["CC"] = str(cc_path) os.environ["CXX"] = str(cxx_path) os.environ["AR"] = str(AR_PATH) return CC_PATH, CXX_PATH, AR_PATH def run_command(command: str, cwd: Path | None = None) -> None: process = RunningProcess(command, cwd=cwd) process.wait() if process.returncode != 0: print(f"{Path(__file__).name}: Error executing command: {command}") sys.exit(1) def compile_fastled(specific_test: str | None = None) -> None: if USE_ZIG: print("USING ZIG COMPILER") rtn = subprocess.run( "python -m ziglang version", shell=True, capture_output=True ).returncode zig_is_installed = rtn == 0 assert ( zig_is_installed ), 'Zig compiler not when using "python -m ziglang version" command' use_zig_compiler() elif USE_CLANG: print("USING CLANG COMPILER") use_clang_compiler() cmake_configure_command_list: list[str] = [ "cmake", "-S", str(PROJECT_ROOT / "tests"), "-B", str(BUILD_DIR), "-G", "Ninja", "-DCMAKE_VERBOSE_MAKEFILE=ON", "-DCMAKE_EXPORT_COMPILE_COMMANDS=ON", ] if WASM_BUILD: cmake_configure_command_list.extend( [ "-DCMAKE_C_COMPILER_TARGET=wasm32-wasi", "-DCMAKE_CXX_COMPILER_TARGET=wasm32-wasi", "-DCMAKE_C_COMPILER_WORKS=TRUE", "-DCMAKE_CXX_COMPILER_WORKS=TRUE", "-DCMAKE_SYSTEM_NAME=Generic", "-DCMAKE_CROSSCOMPILING=TRUE", "-DCMAKE_EXE_LINKER_FLAGS=-Wl,--no-entry -Wl,--export-all -Wl,--lto-O3 -Wl,-z,stack-size=8388608", ] ) cmake_configure_command = subprocess.list2cmdline(cmake_configure_command_list) run_command(cmake_configure_command, cwd=BUILD_DIR) # Build the project if specific_test: cmake_build_command = f"cmake --build {BUILD_DIR} --target test_{specific_test}" else: cmake_build_command = f"cmake --build {BUILD_DIR}" run_command(cmake_build_command) print("FastLED library compiled successfully.") def parse_arguments(): parser = argparse.ArgumentParser( description="Compile FastLED library with different compiler options." ) parser.add_argument("--use-zig", action="store_true", help="Use Zig compiler") parser.add_argument("--use-clang", action="store_true", help="Use Clang compiler") parser.add_argument("--wasm", action="store_true", help="Build for WebAssembly") parser.add_argument( "--clean", action="store_true", help="Clean the build directory before compiling", ) parser.add_argument( "--test", help="Specific test to compile (without test_ prefix)", ) return parser.parse_args() def get_build_info(args: argparse.Namespace) -> dict[str, str | dict[str, str]]: return { "USE_ZIG": str(USE_ZIG), "USE_CLANG": str(USE_CLANG), "WASM_BUILD": str(WASM_BUILD), "CC": os.environ.get("CC", ""), "CXX": os.environ.get("CXX", ""), "AR": os.environ.get("AR", ""), "CFLAGS": os.environ.get("CFLAGS", ""), "CXXFLAGS": os.environ.get("CXXFLAGS", ""), "ARGS": { "use_zig": str(args.use_zig), "use_clang": str(args.use_clang), "wasm": str(args.wasm), }, } def should_clean_build(build_info: dict[str, str | dict[str, str]]) -> bool: build_info_file = BUILD_DIR / "build_info.json" if not build_info_file.exists(): return True with open(build_info_file, "r") as f: old_build_info = json.load(f) return old_build_info != build_info def update_build_info(build_info: dict[str, str | dict[str, str]]): build_info_file = BUILD_DIR / "build_info.json" with open(build_info_file, "w") as f: json.dump(build_info, f, indent=2) def main() -> None: global USE_ZIG, USE_CLANG, WASM_BUILD args = parse_arguments() USE_ZIG = args.use_zig # use Zig's clang compiler USE_CLANG = args.use_clang # Use pure Clang for WASM builds WASM_BUILD = args.wasm using_gcc = not USE_ZIG and not USE_CLANG and not WASM_BUILD if using_gcc: if not shutil.which("g++"): print( "gcc compiler not found in PATH, falling back zig's built in clang compiler" ) USE_ZIG = True USE_CLANG = False if USE_CLANG: if not _has_system_clang_compiler(): print( "Clang compiler not found in PATH, falling back to Zig-clang compiler" ) USE_ZIG = True USE_CLANG = False os.chdir(str(HERE)) print(f"Current directory: {Path('.').absolute()}") build_info = get_build_info(args) if args.clean or should_clean_build(build_info): clean_build_directory() compile_fastled(args.test) update_build_info(build_info) print("FastLED library compiled successfully.") if __name__ == "__main__": main()