516 lines
15 KiB
Python
516 lines
15 KiB
Python
#!/usr/bin/env python3
|
|
import os
|
|
import sys
|
|
import subprocess
|
|
from pathlib import Path
|
|
import shutil
|
|
import platform
|
|
|
|
# ---------------------------------------------------------------------
|
|
# 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"
|
|
|
|
# Platform-specific settings
|
|
PICO_SDK_PATH = os.environ.get("PICO_SDK_PATH", Path.home() / "pico/pico-sdk")
|
|
PICO_BUILD_DIR = Path("build_pico")
|
|
PICO_TOOLCHAIN_PATH = Path("pico_arm_gcc_toolchain.cmake")
|
|
|
|
# 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"]
|
|
|
|
# macOS-specific flags (for cross-compilation if needed)
|
|
MACOS_FLAGS = ["-std=c99", "-Wall", "-Wextra", "-Werror", "-Iinclude", "-g",
|
|
"-mmacosx-version-min=10.13"]
|
|
|
|
# Windows MSVC flags
|
|
MSVC_FLAGS = ["/std:c11", "/Zi", "/Iinclude"]
|
|
MSVC_TEST_FLAGS = MSVC_FLAGS + []
|
|
|
|
# RP2040 toolchain file template
|
|
PICO_TOOLCHAIN_TEMPLATE = """set(CMAKE_SYSTEM_NAME Generic)
|
|
set(CMAKE_SYSTEM_PROCESSOR cortex-m0plus)
|
|
|
|
# Specify the cross compiler
|
|
set(CMAKE_C_COMPILER arm-none-eabi-gcc)
|
|
set(CMAKE_CXX_COMPILER arm-none-eabi-g++)
|
|
set(CMAKE_ASM_COMPILER arm-none-eabi-gcc)
|
|
|
|
# Compiler flags for Cortex-M0+
|
|
set(CMAKE_C_FLAGS_INIT "-mcpu=cortex-m0plus -mthumb")
|
|
set(CMAKE_CXX_FLAGS_INIT "-mcpu=cortex-m0plus -mthumb")
|
|
set(CMAKE_ASM_FLAGS_INIT "-mcpu=cortex-m0plus -mthumb")
|
|
|
|
# Don't run the linker on compiler check
|
|
set(CMAKE_TRY_COMPILE_TARGET_TYPE STATIC_LIBRARY)
|
|
|
|
# Adjust the default behavior of the FIND_XXX() commands:
|
|
# search programs in the host environment
|
|
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
|
|
|
|
# Search headers and libraries in the target environment
|
|
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
|
|
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
|
|
set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY)
|
|
"""
|
|
|
|
# RP2040 settings
|
|
RP2040_CMAKE_TEMPLATE = """cmake_minimum_required(VERSION 3.13)
|
|
|
|
# Pico SDK initialization
|
|
set(PICO_SDK_PATH "{pico_sdk_path}")
|
|
include({pico_sdk_path}/external/pico_sdk_import.cmake)
|
|
|
|
project({project_name} C CXX ASM)
|
|
set(CMAKE_C_STANDARD 11)
|
|
set(CMAKE_CXX_STANDARD 17)
|
|
|
|
pico_sdk_init()
|
|
|
|
# Main executable
|
|
add_executable({project_name}
|
|
{source_files}
|
|
)
|
|
|
|
# Set output name with .elf extension
|
|
set_target_properties({project_name} PROPERTIES
|
|
OUTPUT_NAME "{project_name}.elf"
|
|
SUFFIX ""
|
|
)
|
|
|
|
# Add include directories
|
|
target_include_directories({project_name} PRIVATE
|
|
${{CMAKE_CURRENT_LIST_DIR}}/include
|
|
)
|
|
|
|
# Add compile definitions
|
|
target_compile_definitions({project_name} PRIVATE
|
|
PICO_BUILD=1
|
|
GIT_COMMIT_HASH="{git_hash}"
|
|
)
|
|
|
|
# Link libraries
|
|
target_link_libraries({project_name}
|
|
pico_stdlib
|
|
hardware_uart
|
|
hardware_gpio
|
|
)
|
|
|
|
# Enable USB/UART output
|
|
pico_enable_stdio_usb({project_name} 1)
|
|
pico_enable_stdio_uart({project_name} 1)
|
|
|
|
# Create map/bin/hex/uf2 files
|
|
pico_add_extra_outputs({project_name})
|
|
"""
|
|
|
|
# ---------------------------------------------------------------------
|
|
# PLATFORM DETECTION
|
|
# ---------------------------------------------------------------------
|
|
def detect_platform():
|
|
"""Detect the current operating system"""
|
|
system = platform.system()
|
|
if system == "Darwin":
|
|
return "macos"
|
|
elif system == "Windows":
|
|
return "windows"
|
|
elif system == "Linux":
|
|
return "linux"
|
|
return "unknown"
|
|
|
|
PLATFORM = detect_platform()
|
|
|
|
# ---------------------------------------------------------------------
|
|
# COMPILER DETECTION
|
|
# ---------------------------------------------------------------------
|
|
def detect_compiler(target_platform=None):
|
|
"""Detect appropriate compiler for the target platform"""
|
|
if target_platform == "rp2040":
|
|
return ("arm-none-eabi-gcc", "gcc")
|
|
|
|
target = target_platform or PLATFORM
|
|
|
|
if target == "windows" or os.name == "nt":
|
|
return ("cl", "msvc")
|
|
elif target == "macos":
|
|
# Prefer clang on macOS
|
|
if shutil.which("clang"):
|
|
return ("clang", "gcc")
|
|
return ("gcc", "gcc")
|
|
else:
|
|
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(str(c) for c in 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, target_platform=None):
|
|
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
|
|
|
|
compiler, kind = detect_compiler(target_platform)
|
|
|
|
if kind == "msvc":
|
|
flags = MSVC_TEST_FLAGS if is_test else MSVC_FLAGS
|
|
cmd = [compiler] + flags + ["/Fo" + str(obj), "/c", str(src),
|
|
f"/DGIT_COMMIT_HASH=\"{GIT_HASH}\""]
|
|
else:
|
|
if target_platform == "macos":
|
|
flags = MACOS_FLAGS if not is_test else TEST_FLAGS + ["-mmacosx-version-min=10.13"]
|
|
else:
|
|
flags = TEST_FLAGS if is_test else COMMON_FLAGS
|
|
|
|
cmd = [compiler] + 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, target_platform=None):
|
|
mkdir(BIN_DIR)
|
|
compiler, kind = detect_compiler(target_platform)
|
|
|
|
if kind == "msvc":
|
|
cmd = [compiler] + list(map(str, objects)) + ["/Fe" + str(output)]
|
|
else:
|
|
cmd = [compiler] + 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
|
|
|
|
|
|
# ---------------------------------------------------------------------
|
|
# RP2040 BUILD
|
|
# ---------------------------------------------------------------------
|
|
def check_pico_sdk():
|
|
"""Check if Pico SDK is available"""
|
|
sdk_path = Path(PICO_SDK_PATH)
|
|
if not sdk_path.exists():
|
|
print(f"ERROR: Pico SDK not found at {sdk_path}")
|
|
print("Please set PICO_SDK_PATH environment variable or install SDK at ~/pico/pico-sdk")
|
|
return False
|
|
|
|
# Check for ARM toolchain
|
|
if not shutil.which("arm-none-eabi-gcc"):
|
|
print("ERROR: ARM toolchain not found!")
|
|
print("Please install arm-none-eabi-gcc:")
|
|
print(" Ubuntu/Debian: sudo apt install gcc-arm-none-eabi")
|
|
print(" macOS: brew install arm-none-eabi-gcc")
|
|
return False
|
|
|
|
return True
|
|
|
|
|
|
def generate_pico_toolchain():
|
|
"""Generate ARM GCC toolchain file for CMake"""
|
|
with open(PICO_TOOLCHAIN_PATH, "w") as f:
|
|
f.write(PICO_TOOLCHAIN_TEMPLATE)
|
|
print(f"Generated {PICO_TOOLCHAIN_PATH}")
|
|
|
|
|
|
def generate_pico_cmake(project_name="sls"):
|
|
"""Generate CMakeLists.txt for RP2040 build"""
|
|
sources = list(SRC_DIR.glob("*.c"))
|
|
# Exclude main.c, repl.c, file.c and test files for Pico build
|
|
# pico_main.c will provide its own REPL implementation
|
|
sources = [s for s in sources
|
|
if s.name not in ["main.c", "repl.c", "file.c"]
|
|
and "test" not in s.stem.lower()]
|
|
|
|
# Check for pico_main.c
|
|
pico_main = SRC_DIR / "pico_main.c"
|
|
if not pico_main.exists():
|
|
print(f"WARNING: {pico_main} not found! Please create it.")
|
|
print("The Pico build requires a pico_main.c file.")
|
|
return False
|
|
|
|
sources.append(pico_main)
|
|
|
|
source_files = "\n".join(f" {s}" for s in sources)
|
|
|
|
cmake_content = RP2040_CMAKE_TEMPLATE.format(
|
|
project_name=project_name,
|
|
source_files=source_files,
|
|
git_hash=GIT_HASH,
|
|
pico_sdk_path=PICO_SDK_PATH
|
|
)
|
|
|
|
cmake_file = Path("CMakeLists.txt")
|
|
with open(cmake_file, "w") as f:
|
|
f.write(cmake_content)
|
|
|
|
print(f"Generated {cmake_file}")
|
|
return True
|
|
|
|
|
|
def build_rp2040():
|
|
"""Build for RP2040 using CMake"""
|
|
if not check_pico_sdk():
|
|
return False
|
|
|
|
print("\n=== Building for RP2040 ===\n")
|
|
|
|
# Generate toolchain file
|
|
generate_pico_toolchain()
|
|
|
|
# Generate CMakeLists.txt
|
|
if not generate_pico_cmake():
|
|
return False
|
|
|
|
# Create build directory
|
|
mkdir(PICO_BUILD_DIR)
|
|
|
|
# Run CMake with explicit toolchain
|
|
cmake_cmd = [
|
|
"cmake",
|
|
"-B", str(PICO_BUILD_DIR),
|
|
"-S", ".",
|
|
f"-DCMAKE_TOOLCHAIN_FILE={PICO_TOOLCHAIN_PATH}"
|
|
]
|
|
run(cmake_cmd)
|
|
|
|
# Build
|
|
build_cmd = ["cmake", "--build", str(PICO_BUILD_DIR), "-j4"]
|
|
run(build_cmd)
|
|
|
|
# Show output files
|
|
uf2_file = PICO_BUILD_DIR / "sls.uf2"
|
|
if uf2_file.exists():
|
|
print(f"\nBuild successful! UF2 file: {uf2_file}")
|
|
print("To flash: Copy this file to your Pico in BOOTSEL mode")
|
|
|
|
return True
|
|
|
|
|
|
# ---------------------------------------------------------------------
|
|
# MACOS BUILD
|
|
# ---------------------------------------------------------------------
|
|
def build_macos():
|
|
"""Build for macOS (can be run on macOS or cross-compile on Linux)"""
|
|
print("\n=== Building for macOS ===\n")
|
|
|
|
if PLATFORM != "macos" and PLATFORM != "linux":
|
|
print("ERROR: macOS builds require macOS or Linux with osxcross")
|
|
return False
|
|
|
|
sources = list(SRC_DIR.glob("*.c"))
|
|
objects = [compile_source(s, is_test=False, target_platform="macos") for s in sources]
|
|
|
|
macos_target = BIN_DIR / "sls_macos"
|
|
link_executable(objects, macos_target, target_platform="macos")
|
|
|
|
print(f"\nBuild successful! Binary: {macos_target}")
|
|
return True
|
|
|
|
|
|
# ---------------------------------------------------------------------
|
|
# TARGETS
|
|
# ---------------------------------------------------------------------
|
|
def build_main():
|
|
sources = list(SRC_DIR.glob("*.c"))
|
|
objects = [compile_source(s, is_test=False) for s in sources if s.name != 'pico_main.c']
|
|
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)
|
|
shutil.rmtree(PICO_BUILD_DIR, ignore_errors=True)
|
|
cmake_file = Path("CMakeLists.txt")
|
|
if cmake_file.exists():
|
|
cmake_file.unlink()
|
|
toolchain_file = PICO_TOOLCHAIN_PATH
|
|
if toolchain_file.exists():
|
|
toolchain_file.unlink()
|
|
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)])
|
|
|
|
|
|
def show_help():
|
|
help_text = """
|
|
Build Script for Multi-Platform Compilation
|
|
============================================
|
|
|
|
Usage: python3 build.py [command]
|
|
|
|
Commands:
|
|
all, main, build Build for current platform
|
|
run Build and run program
|
|
test Build and run tests
|
|
debug Build tests and run debugger
|
|
clean Remove build artifacts
|
|
|
|
macos Build for macOS
|
|
pico, rp2040 Build for RP2040 (Raspberry Pi Pico)
|
|
|
|
help Show this help message
|
|
|
|
Environment Variables:
|
|
PICO_SDK_PATH Path to Pico SDK (default: ~/pico/pico-sdk)
|
|
|
|
Examples:
|
|
python3 build.py build # Build for current platform
|
|
python3 build.py pico # Build for RP2040
|
|
python3 build.py macos # Build for macOS
|
|
python3 build.py clean # Clean all build files
|
|
"""
|
|
print(help_text)
|
|
|
|
|
|
# ---------------------------------------------------------------------
|
|
# ENTRY POINT
|
|
# ---------------------------------------------------------------------
|
|
def main():
|
|
if len(sys.argv) < 2:
|
|
show_help()
|
|
return
|
|
|
|
cmd = sys.argv[1]
|
|
|
|
match cmd:
|
|
case "all" | "main" | "build":
|
|
build_main()
|
|
case "run":
|
|
run_main()
|
|
case "test":
|
|
run_tests()
|
|
case "debug":
|
|
debug_tests()
|
|
case "clean":
|
|
clean()
|
|
case "macos":
|
|
build_macos()
|
|
case "pico" | "rp2040":
|
|
build_rp2040()
|
|
case "help" | "-h" | "--help":
|
|
show_help()
|
|
case _:
|
|
print(f"Unknown target: {cmd}")
|
|
print("Run 'python3 build.py help' for usage information")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|