cmake_minimum_required(VERSION 3.10) project(fun VERSION 0.41.5 LANGUAGES C) set(CMAKE_C_STANDARD 99) set(CMAKE_C_STANDARD_REQUIRED ON) include(GNUInstallDirs) include(${CMAKE_SOURCE_DIR}/cmake/Options.cmake) include(${CMAKE_SOURCE_DIR}/cmake/Dependencies.cmake) include(${CMAKE_SOURCE_DIR}/cmake/Extensions/Extensions.cmake) include(${CMAKE_SOURCE_DIR}/cmake/Targets.cmake) # --- Always show key build toggles --- # Normalize to ENABLED/DISABLED like the "Fun extension summary" if(FUN_DEBUG) set(_FUN_DEBUG_STATE "ENABLED") else() set(_FUN_DEBUG_STATE "DISABLED") endif() if(FUN_USE_MUSL) set(_FUN_USE_MUSL_STATE "ENABLED") else() set(_FUN_USE_MUSL_STATE "DISABLED") endif() if(FUN_WITH_CPP) set(_FUN_WITH_CPP_STATE "ENABLED") else() set(_FUN_WITH_CPP_STATE "DISABLED") endif() if(FUN_WITH_RUST) set(_FUN_WITH_RUST_STATE "ENABLED") else() set(_FUN_WITH_RUST_STATE "DISABLED") endif() if(FUN_BUILD_DOCS) set(_FUN_BUILD_DOCS "ENABLED") else() set(_FUN_BUILD_DOCS "DISABLED") endif() message(STATUS "==== Fun build options ====") message(STATUS " FUN_DEBUG: ${_FUN_DEBUG_STATE}") message(STATUS " FUN_USE_MUSL: ${_FUN_USE_MUSL_STATE}") message(STATUS " FUN_WITH_CPP: ${_FUN_WITH_CPP_STATE}") message(STATUS " FUN_WITH_RUST: ${_FUN_WITH_RUST_STATE}") message(STATUS " FUN_BUILD_DOCS: ${_FUN_BUILD_DOCS}") message(STATUS "===========================") # Convenience aggregate target (like 'build' in Makefile) add_custom_target(build DEPENDS fun funstx fun_test test_opcodes ) # Formatting helper target: run clang-format over C/C++ sources using .clang-format add_custom_target(format COMMAND ${CMAKE_COMMAND} -E echo "Running clang-format on sources..." COMMAND /bin/sh -c "command -v clang-format >/dev/null 2>&1 || { echo 'clang-format not found in PATH' >&2; exit 1; }" COMMAND /bin/sh -c "set -e; for pat in '*.c' '*.h' '*.cpp' '*.hpp'; do find ${CMAKE_SOURCE_DIR}/src -type f -name \"\$pat\" -print0; done | xargs -0 -n 50 clang-format -i" WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} COMMENT "Apply .clang-format (IndentWidth=2) to all source files" ) # --- Rust (Cargo) integration: optionally build and link a staticlib with opcode examples --- option(FUN_WITH_RUST "Build and link Rust opcode library" OFF) if(FUN_WITH_RUST) find_program(CARGO_EXECUTABLE cargo) if(NOT CARGO_EXECUTABLE) message(FATAL_ERROR "FUN_WITH_RUST=ON but 'cargo' not found in PATH") endif() set(RUST_CRATE_DIR ${CMAKE_SOURCE_DIR}/src/rust) # Map CMake build type to Cargo profile and output path if(CMAKE_BUILD_TYPE STREQUAL "Debug") set(RUST_PROFILE "debug") set(RUST_BUILD_ARGS) else() set(RUST_PROFILE "release") set(RUST_BUILD_ARGS --release) endif() # Crate name as per Cargo.toml # Crate name on-disk replaces '-' with '_' in artifact names set(RUST_CRATE_NAME hello-c-world) string(REPLACE "-" "_" RUST_CRATE_BASENAME "${RUST_CRATE_NAME}") set(RUST_LIB_NAME lib${RUST_CRATE_BASENAME}.a) set(RUST_LIB_PATH ${RUST_CRATE_DIR}/target/${RUST_PROFILE}/${RUST_LIB_NAME}) add_custom_command( OUTPUT ${RUST_LIB_PATH} COMMAND ${CARGO_EXECUTABLE} build ${RUST_BUILD_ARGS} WORKING_DIRECTORY ${RUST_CRATE_DIR} COMMENT "Building Rust static library (${RUST_PROFILE})" VERBATIM ) add_custom_target(rust_ops_build DEPENDS ${RUST_LIB_PATH}) add_library(fun_ops STATIC IMPORTED GLOBAL) set_target_properties(fun_ops PROPERTIES IMPORTED_LOCATION ${RUST_LIB_PATH} IMPORTED_LINK_INTERFACE_LANGUAGES C ) add_dependencies(fun_ops rust_ops_build) # Link Rust ops into the core so executables can call them if(TARGET fun_core) add_dependencies(fun_core rust_ops_build) target_link_libraries(fun_core PRIVATE fun_ops) target_compile_definitions(fun_core PRIVATE FUN_WITH_RUST) endif() # Propagate define to test targets that might call Rust if(TARGET test_opcodes) add_dependencies(test_opcodes rust_ops_build) target_link_libraries(test_opcodes PRIVATE fun_ops) target_compile_definitions(test_opcodes PRIVATE FUN_WITH_RUST) endif() if(TARGET fun) target_compile_definitions(fun PRIVATE FUN_WITH_RUST) endif() endif() # --- Optional C++ opcode demo --- option(FUN_WITH_CPP "Build and link C++ opcode demo" OFF) if(FUN_WITH_CPP) enable_language(CXX) add_library(fun_cpp_ops STATIC src/vm/cpp/add.cpp ) # Compile definitions propagate to C targets that dispatch the opcode target_compile_definitions(fun_cpp_ops PUBLIC FUN_WITH_CPP) target_include_directories(fun_cpp_ops PUBLIC ${CMAKE_SOURCE_DIR}/src) # Link the C++ ops into the core so executables can call them if(TARGET fun_core) target_link_libraries(fun_core PRIVATE fun_cpp_ops) target_compile_definitions(fun_core PRIVATE FUN_WITH_CPP) endif() # Also propagate to test targets (if they might use it) if(TARGET test_opcodes) target_link_libraries(test_opcodes PRIVATE fun_cpp_ops) target_compile_definitions(test_opcodes PRIVATE FUN_WITH_CPP) endif() if(TARGET fun) target_compile_definitions(fun PRIVATE FUN_WITH_CPP) endif() endif() # --- Size optimization for final binaries (Release) --- # Enable function/data sectioning for better GC; safe for all configs. add_compile_options(-ffunction-sections -fdata-sections) # Link-time garbage collection and stripping for the main executable in Release if(TARGET fun) # Garbage-collect unused sections; also strip symbols (-s) in Release target_link_options(fun PRIVATE $<$:-Wl,--gc-sections -s> ) # Prefer enabling LTO/IPO for Release builds set_property(TARGET fun PROPERTY INTERPROCEDURAL_OPTIMIZATION_RELEASE TRUE) endif() if(TARGET fun_core) # LTO/IPO for the core library helps the final link DCE more Rust/C glue set_property(TARGET fun_core PROPERTY INTERPROCEDURAL_OPTIMIZATION_RELEASE TRUE) # Ensure objects compiled with sectioning (added globally above) benefit from GC target_link_options(fun_core PRIVATE $<$:-Wl,--gc-sections>) endif() # Convenience targets: repl, run, threads-demo, ops, examples set(FUN_RUN_SCRIPT "" CACHE STRING "Script to run with the 'run' target, e.g. -DFUN_RUN_SCRIPT=examples/strings_test.fun") if(FUN_WITH_REPL) add_custom_target(repl COMMAND $ DEPENDS fun USES_TERMINAL COMMENT "Run Fun REPL" ) endif() add_custom_target(run COMMAND $ ${FUN_RUN_SCRIPT} DEPENDS fun USES_TERMINAL COMMENT "Run Fun with script: ${FUN_RUN_SCRIPT}" ) add_custom_target(ops COMMAND ${_FUN_PY} ${CMAKE_SOURCE_DIR}/scripts/check_op_includes.py --verbose WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} USES_TERMINAL COMMENT "Verbose opcode include check" ) add_custom_target(ops-quiet COMMAND ${_FUN_PY} ${CMAKE_SOURCE_DIR}/scripts/check_op_includes.py WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} USES_TERMINAL COMMENT "Opcode include check" ) # Clean and distclean add_custom_target(fun_clean COMMAND ${CMAKE_COMMAND} --build ${CMAKE_BINARY_DIR} --target clean COMMENT "Clean build outputs (objects, binaries) in the build directory" ) add_custom_target(distclean COMMAND ${CMAKE_COMMAND} --build ${CMAKE_BINARY_DIR} --target clean COMMAND ${CMAKE_COMMAND} -E rm -rf ${CMAKE_BINARY_DIR}/CMakeCache.txt ${CMAKE_BINARY_DIR}/CMakeFiles ${CMAKE_BINARY_DIR}/cmake_install.cmake ${CMAKE_BINARY_DIR}/install_manifest.txt ${CMAKE_BINARY_DIR}/Makefile ${CMAKE_BINARY_DIR}/*.ninja ${CMAKE_BINARY_DIR}/.ninja_* COMMENT "Remove build outputs and CMake-generated files (reconfigure needed)" ) # Robust uninstall support as a function (uses install_manifest*.txt) function(fun_add_uninstall_target) set(_FUN_UNINSTALL_SCRIPT "${CMAKE_BINARY_DIR}/cmake_uninstall.cmake") # Generate the cmake_uninstall.cmake script at configure time file(WRITE "${_FUN_UNINSTALL_SCRIPT}" "set(_candidates \"\") # Prefer explicit MANIFEST if provided if(DEFINED MANIFEST AND NOT MANIFEST STREQUAL \"\") list(APPEND _candidates \"\${MANIFEST}\") endif() # Common locations (out-of-source and legacy in-source builds) list(APPEND _candidates \"\${CMAKE_BINARY_DIR}/install_manifest.txt\" \"\${CMAKE_BINARY_DIR}/install_manifest_Debug.txt\" \"\${CMAKE_BINARY_DIR}/install_manifest_Release.txt\" \"\${CMAKE_BINARY_DIR}/install_manifest_RelWithDebInfo.txt\" \"\${CMAKE_BINARY_DIR}/install_manifest_MinSizeRel.txt\" ) set(_manifest_file \"\") foreach(_cand IN LISTS _candidates) if(EXISTS \"\${_cand}\") set(_manifest_file \"\${_cand}\") break() endif() endforeach() if(NOT _manifest_file) message(FATAL_ERROR \"Cannot find install manifest. Tried: \${_candidates}. If you installed from a different build directory, run uninstall from that build directory (where install_manifest*.txt resides).\") endif() file(READ \"\${_manifest_file}\" _manifest) string(REPLACE \"\\n\" \";\" _files \"\${_manifest}\") set(_removed 0) set(_dirs \"\") foreach(_file IN LISTS _files) if(_file STREQUAL \"\") continue() endif() if(EXISTS \"\${_file}\" OR IS_SYMLINK \"\${_file}\") message(STATUS \"Uninstalling: \${_file}\") file(REMOVE \"\${_file}\") # If the file still exists after attempting to remove it, fail with an explicit error if(EXISTS \"\${_file}\" OR IS_SYMLINK \"\${_file}\") message(FATAL_ERROR \"Failed to remove: \${_file}. Try running the uninstall target with sudo if it is a system install.\") endif() # Track the containing directory for potential cleanup get_filename_component(_dir \"\${_file}\" DIRECTORY) list(APPEND _dirs \"\${_dir}\") math(EXPR _removed \"\${_removed}+1\") else() message(STATUS \"Skipping (not found): \${_file}\") endif() endforeach() # Remove empty directories that contained installed files (and their parents under safe roots) # Safe roots we are allowed to prune if they become empty: set(_safe_roots \"/usr/share/fun\" \"/usr/share/doc/fun\") # Expand to include parent directories up to the safe roots set(_all_dirs \"\") foreach(_d IN LISTS _dirs) set(_p \"\${_d}\") while(NOT \"\${_p}\" STREQUAL \"\" AND NOT \"\${_p}\" STREQUAL \"/\") list(APPEND _all_dirs \"\${_p}\") get_filename_component(_p \"\${_p}\" DIRECTORY) # Stop collecting parents once we reach a safe root or the filesystem root list(FIND _safe_roots \"\${_p}\" _root_idx) if(_root_idx GREATER -1) list(APPEND _all_dirs \"\${_p}\") break() endif() endwhile() endforeach() # Deduplicate list(REMOVE_DUPLICATES _all_dirs) # Try multiple passes to remove parents that become empty after children are removed set(_removed_dirs 0) set(_pass 0) while(TRUE) set(_pass_removed 0) foreach(_dir IN LISTS _all_dirs) if(EXISTS \"\${_dir}\" AND IS_DIRECTORY \"\${_dir}\") # Only act on directories within safe roots set(_allowed FALSE) foreach(_root IN LISTS _safe_roots) if(\"\${_dir}\" MATCHES \"^\${_root}(/|\$)\") set(_allowed TRUE) break() endif() endforeach() if(NOT _allowed) # e.g. /usr/bin — do not remove continue() endif() # Remove only if empty (do not recurse unrelated content) file(GLOB _dir_contents LIST_DIRECTORIES true \"\${_dir}/*\") list(LENGTH _dir_contents _dir_len) if(_dir_len EQUAL 0) message(STATUS \"Removing empty directory: \${_dir}\") file(REMOVE_RECURSE \"\${_dir}\") if(IS_DIRECTORY \"\${_dir}\") message(FATAL_ERROR \"Failed to remove directory: \${_dir}. Try running the uninstall target with sudo if it is a system install.\") endif() math(EXPR _removed_dirs \"\${_removed_dirs}+1\") math(EXPR _pass_removed \"\${_pass_removed}+1\") endif() endif() endforeach() math(EXPR _pass \"\${_pass}+1\") if(_pass_removed EQUAL 0 OR _pass GREATER 5) break() endif() endwhile() message(STATUS \"Uninstall finished. Removed \${_removed} files and \${_removed_dirs} directories (if empty) listed in \${_manifest_file}\") ") # Expose an 'uninstall' build target (run with sudo if system locations were used) add_custom_target(uninstall COMMAND ${CMAKE_COMMAND} -D MANIFEST="${CMAKE_BINARY_DIR}/install_manifest.txt" -D CMAKE_BINARY_DIR="${CMAKE_BINARY_DIR}" -D CMAKE_SOURCE_DIR="${CMAKE_SOURCE_DIR}" -P "${_FUN_UNINSTALL_SCRIPT}" USES_TERMINAL COMMENT "Uninstall files installed by this project (use sudo if needed)" ) endfunction() # Define the uninstall target fun_add_uninstall_target() # --- CTest integration: run crypto examples as self-tests --- # These examples print results and exit non-zero on mismatch, so we can # wire them into CTest to have CI verify cryptographic correctness. include(CTest) if(BUILD_TESTING) enable_testing() # Helper to register a Fun example as a CTest test function(fun_add_example_test TEST_NAME SCRIPT_REL) # Absolute path to the example script in source tree set(_script "${CMAKE_SOURCE_DIR}/${SCRIPT_REL}") if(NOT EXISTS "${_script}") message(FATAL_ERROR "fun_add_example_test: script not found: ${_script}") endif() add_test(NAME ${TEST_NAME} COMMAND $ "${_script}" WORKING_DIRECTORY ${CMAKE_BINARY_DIR} ) # Ensure the interpreter can find standard library (*.fun) # The examples were authored to respect FUN_LIB_DIR. set_tests_properties(${TEST_NAME} PROPERTIES ENVIRONMENT "FUN_LIB_DIR=${CMAKE_SOURCE_DIR}/lib" ) endfunction() # Crypto example self-tests fun_add_example_test(crypto_md5 examples/crypto/md5_demo.fun) fun_add_example_test(crypto_sha1 examples/crypto/sha1_demo.fun) fun_add_example_test(crypto_sha256_hex examples/crypto/sha256_demo.fun) fun_add_example_test(crypto_sha256_str examples/crypto/sha256_str_demo.fun) fun_add_example_test(crypto_sha512_hex examples/crypto/sha512_demo.fun) fun_add_example_test(crypto_sha512_str examples/crypto/sha512_str_demo.fun) fun_add_example_test(crypto_sha384 examples/crypto/sha384_example.fun) fun_add_example_test(crypto_crc32 examples/crypto/crc32_example.fun) fun_add_example_test(crypto_crc32c examples/crypto/crc32c_example.fun) fun_add_example_test(crypto_aes256 examples/crypto/aes256.fun) # Include-line mapping regression test: # This script intentionally triggers a runtime error inside an included file. # The VM augments the error with a precise (file:line) from the included file. # We assert that stderr/stdout contain the correct mapped location and that the # program exits with a non-zero status (WILL_FAIL). set(_inc_map_script "${CMAKE_SOURCE_DIR}/examples/error/include_line_mapping_test.fun") if(EXISTS "${_inc_map_script}") add_test(NAME include_line_mapping COMMAND $ "${_inc_map_script}" WORKING_DIRECTORY ${CMAKE_BINARY_DIR} ) # Ensure the interpreter resolves <...> includes to the repo lib/ set_tests_properties(include_line_mapping PROPERTIES ENVIRONMENT "FUN_LIB_DIR=${CMAKE_SOURCE_DIR}/lib" # The runtime error message should contain the mapped file path and line 7 PASS_REGULAR_EXPRESSION "lib/test/include_divzero.fun:7" WILL_FAIL TRUE ) else() message(WARNING "include_line_mapping_test.fun not found; skipping include_line_mapping CTest") endif() endif() # --- Doxygen docs target (optional) --- # Generate API documentation for everything under src/ into /docs/html. # Disabled by default; enable with -DFUN_BUILD_DOCS=ON option(FUN_BUILD_DOCS "Enable Doxygen documentation target 'fun_docs'" OFF) if(FUN_BUILD_DOCS) find_package(Doxygen REQUIRED dot) # Provide a boolean for HAVE_DOT in the Doxyfile set(HAVE_DOT NO) if (DOXYGEN_DOT_FOUND) set(HAVE_DOT YES) endif() # Determine documentation version: prefer `git describe`, fallback to project version set(FUN_DOCS_VERSION "${PROJECT_VERSION}") find_package(Git QUIET) if(GIT_FOUND) execute_process( COMMAND ${GIT_EXECUTABLE} describe --tags --dirty --always WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} OUTPUT_VARIABLE _git_desc OUTPUT_STRIP_TRAILING_WHITESPACE ERROR_QUIET ) if(_git_desc) set(FUN_DOCS_VERSION "${_git_desc}") endif() endif() # Optional logo (if present in the website assets) set(FUN_DOCS_LOGO "") set(_fun_logo_path "${CMAKE_SOURCE_DIR}/web/images/fun-square.png") if(EXISTS "${_fun_logo_path}") set(FUN_DOCS_LOGO "${_fun_logo_path}") endif() set(DOXYGEN_IN ${CMAKE_SOURCE_DIR}/Doxygen.in) set(DOXYGEN_OUT ${CMAKE_CURRENT_BINARY_DIR}/Doxyfile) configure_file(${DOXYGEN_IN} ${DOXYGEN_OUT} @ONLY) # Create a target to run Doxygen with the configured file add_custom_target(fun_docs COMMAND ${DOXYGEN_EXECUTABLE} ${DOXYGEN_OUT} WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} COMMENT "Generating API documentation with Doxygen" VERBATIM) endif() # Install rules # Binary install(TARGETS fun RUNTIME DESTINATION /usr/bin ) # Also install the syntax-checker CLI by default install(TARGETS funstx RUNTIME DESTINATION /usr/bin ) # Libs install(DIRECTORY lib/ DESTINATION /usr/share/fun/lib FILES_MATCHING PATTERN "*.fun" ) # Optionally install example scripts option(FUN_INSTALL_EXAMPLES "Install example .fun scripts" ON) if(FUN_INSTALL_EXAMPLES) install(DIRECTORY examples/ DESTINATION /usr/share/fun/examples FILES_MATCHING PATTERN "*.fun" ) endif() # - Docs install(FILES README.md LICENSE DESTINATION /usr/share/doc/fun ) # - Manpages # Install section 1 manpages for the CLI tools install(FILES man/fun.1 man/funstx.1 DESTINATION /usr/share/man/man1 )