This change addresses a critical issue where common variable names in library functions (like 'i', 'n', 'src') were being treated as globals by default, leading to state collisions and unexpected side effects across different files. Changes: - Modified `src/parser.c` to automatically treat new variables assigned within a function body as locals unless they were already declared as globals. - Introduced `sym_find` in the parser to distinguish between finding an existing global and creating a new one. - Reverted `lib/net/cgi.fun` to use standard, short variable names, confirming that they no longer interfere with the caller's scope. - Verified that explicit global updates still work if the variable was already defined at the top-level scope.
465 lines
16 KiB
CMake
465 lines
16 KiB
CMake
cmake_minimum_required(VERSION 3.10)
|
|
project(fun VERSION 0.41.0 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()
|
|
|
|
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 "===========================")
|
|
|
|
# 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
|
|
$<$<CONFIG:Release>:-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 $<$<CONFIG:Release>:-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 $<TARGET_FILE:fun>
|
|
DEPENDS fun
|
|
USES_TERMINAL
|
|
COMMENT "Run Fun REPL"
|
|
)
|
|
endif()
|
|
|
|
add_custom_target(run
|
|
COMMAND $<TARGET_FILE:fun> ${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 $<TARGET_FILE:fun> "${_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 $<TARGET_FILE:fun> "${_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()
|
|
|
|
# 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
|
|
)
|