diff --git a/.clang-format b/.clang-format index bffce04a3e..30f77e23e3 100644 --- a/.clang-format +++ b/.clang-format @@ -106,5 +106,5 @@ SpacesBeforeTrailingComments: 1 # SpacesInParentheses: false # SpacesInSquareBrackets: false TabWidth: 4 -UseCRLF: true +UseCRLF: false UseTab: Always diff --git a/.editorconfig b/.editorconfig index 46511e1253..33f1a3bf62 100644 --- a/.editorconfig +++ b/.editorconfig @@ -2,7 +2,7 @@ root = true [*] charset = utf-8 -end_of_line = crlf +end_of_line = lf insert_final_newline = true trim_trailing_whitespace = true indent_style = tab diff --git a/.github/workflows/clang-format.yml b/.github/workflows/clang-format.yml index aceeb9c8ee..a0df4da426 100644 --- a/.github/workflows/clang-format.yml +++ b/.github/workflows/clang-format.yml @@ -11,7 +11,7 @@ jobs: name: Formatting Check runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v6.0.2 - name: Run clang-format style check for C/C++. uses: jidicula/clang-format-action@v4.9.0 with: diff --git a/.github/workflows/docker-hub.yml b/.github/workflows/docker-hub.yml index 15ca42b072..c34506a0d7 100644 --- a/.github/workflows/docker-hub.yml +++ b/.github/workflows/docker-hub.yml @@ -141,7 +141,7 @@ jobs: - name: Upload digest if: (github.ref == 'refs/heads/master' || startsWith(github.ref, 'refs/tags/')) && github.event_name != 'pull_request' - uses: actions/upload-artifact@v6.0.0 + uses: actions/upload-artifact@v7.0.0 with: name: digests-${{ env.PLATFORM_SAFE }} path: .bake/digests/* diff --git a/CMakeLists.txt b/CMakeLists.txt index 2e5516c038..41b38f9c78 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -317,9 +317,29 @@ install(FILES README.md DESTINATION ${INSTALL_ROOT} COMPONENT runtime) # if(OPTION_GIT_HOOKS) - message(STATUS "Installing git hooks at ${CMAKE_CURRENT_SOURCE_DIR}") + # macOS shares the POSIX hook scripts with Linux/BSD; there is no + # separate githooks/macos/ directory. + if(PROJECT_OS_FAMILY STREQUAL "macos") + set(_hooks_family "unix") + else() + set(_hooks_family "${PROJECT_OS_FAMILY}") + endif() + + message(STATUS "Installing git hooks at ${CMAKE_CURRENT_SOURCE_DIR}/githooks/${_hooks_family}") + execute_process( - COMMAND git config --local core.hooksPath githooks/${PROJECT_OS_FAMILY} + COMMAND git config --local core.hooksPath githooks/${_hooks_family} WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} + RESULT_VARIABLE _git_hooks_result ) + + if(NOT _git_hooks_result EQUAL 0) + message(WARNING + "Failed to install git hooks (is git available and is this a git repository?). " + "Run manually: git config --local core.hooksPath githooks/${_hooks_family}" + ) + endif() + + unset(_hooks_family) + unset(_git_hooks_result) endif() diff --git a/cmake/ClangDevTools.cmake b/cmake/ClangDevTools.cmake index 3013037744..88a07f20f8 100644 --- a/cmake/ClangDevTools.cmake +++ b/cmake/ClangDevTools.cmake @@ -1,5 +1,5 @@ # See FindClangFormat.cmake -# Variables of interest on this file: ${ClangFormat_VERSION} and ${ClangFormat_EXECUTABLE} +# Variables of interest on this file: ${ClangFormat_VERSION}, ${ClangFormat_EXECUTABLE}, ${ClangFormat_USE_DOCKER} # Get only C/C++ files for now file(GLOB_RECURSE @@ -16,6 +16,36 @@ file(GLOB_RECURSE ${CMAKE_SOURCE_DIR}/source/**/*.inl ) +if(ClangFormat_USE_DOCKER) + message(STATUS + "clang-format target will run via Docker (ghcr.io/jidicula/clang-format:12). " + "Pulling image now to avoid latency at build time..." + ) + + # Pull the image at configure time so `cmake --build . --target clang-format` + # does not stall on the first run waiting for the pull. + execute_process( + COMMAND docker pull ghcr.io/jidicula/clang-format:12 + RESULT_VARIABLE _docker_pull_result + OUTPUT_QUIET + ERROR_QUIET + ) + + if(NOT _docker_pull_result EQUAL 0) + message(WARNING + "Failed to pull ghcr.io/jidicula/clang-format:12 at configure time. " + "The image will be pulled on the first `clang-format` build target invocation, " + "or the pull may fail if there is no network access / Docker daemon is not running." + ) + else() + message(STATUS "Docker image ghcr.io/jidicula/clang-format:12 is ready.") + endif() + + unset(_docker_pull_result) +else() + message(STATUS "clang-format target will use native binary: ${ClangFormat_EXECUTABLE} (version ${ClangFormat_VERSION})") +endif() + # clang-tidy not implemented yet #add_custom_target( # clang-tidy @@ -34,4 +64,5 @@ add_custom_target( -style=file -i ${ALL_SOURCE_FILES} + COMMENT "Running clang-format 12 on all source files..." ) diff --git a/cmake/CompileOptions.cmake b/cmake/CompileOptions.cmake index f82baad27b..b11bf5bc62 100644 --- a/cmake/CompileOptions.cmake +++ b/cmake/CompileOptions.cmake @@ -109,6 +109,26 @@ else() set(TESTS_MEMCHECK_ENVIRONMENT_VARIABLES) endif() +# Warn loudly when a sanitizer is requested with a build type that will not +# activate it. Silent no-ops here cost hours of debugging as happened in node_ci. +foreach(_san_opt + OPTION_BUILD_THREAD_SANITIZER + OPTION_BUILD_MEMORY_SANITIZER + OPTION_BUILD_ADDRESS_SANITIZER +) + if(${_san_opt} AND + NOT CMAKE_BUILD_TYPE STREQUAL "Debug" AND + NOT CMAKE_BUILD_TYPE STREQUAL "RelWithDebInfo") + message(WARNING + "${_san_opt} is ON but CMAKE_BUILD_TYPE is '${CMAKE_BUILD_TYPE}'. " + "Sanitizers only activate for Debug and RelWithDebInfo builds - " + "the sanitizer will have NO EFFECT. " + "Reconfigure with -DCMAKE_BUILD_TYPE=Debug to enable it." + ) + endif() +endforeach() +unset(_san_opt) + # ThreadSanitizer is incompatible with AddressSanitizer and LeakSanitizer if(OPTION_BUILD_THREAD_SANITIZER AND (CMAKE_BUILD_TYPE STREQUAL "Debug" OR CMAKE_BUILD_TYPE STREQUAL "RelWithDebInfo")) set(SANITIZER_LIBRARIES -ltsan) diff --git a/cmake/FindClangFormat.cmake b/cmake/FindClangFormat.cmake index 3e7e79d433..1c29d3d32b 100644 --- a/cmake/FindClangFormat.cmake +++ b/cmake/FindClangFormat.cmake @@ -1,98 +1,261 @@ # -# Taken from https://raw.githubusercontent.com/BlueBrain/git-cmake-format/master/FindClangFormat.cmake -# --------------- +# FindClangFormat.cmake +# Finds clang-format version 12 on the host system. +# If not found natively, falls back to a Docker wrapper using +# ghcr.io/jidicula/clang-format:12 # -# The module defines the following variables +# Exported variables: +# ClangFormat_EXECUTABLE - Path to clang-format executable (or generated wrapper script) +# ClangFormat_FOUND - TRUE if clang-format 12.x was found or Docker fallback is available +# ClangFormat_VERSION - Full version string (e.g. "12.0.1") +# ClangFormat_VERSION_MAJOR - Major version component +# ClangFormat_VERSION_MINOR - Minor version component +# ClangFormat_VERSION_PATCH - Patch version component +# ClangFormat_VERSION_COUNT - Number of version components +# ClangFormat_USE_DOCKER - TRUE when the Docker fallback is active # -# ``ClangFormat_EXECUTABLE`` Path to clang-format executable -# ``ClangFormat_FOUND`` True if the clang-format executable was found. -# ``ClangFormat_VERSION`` The version of clang-format found -# ``ClangFormat_VERSION_MAJOR`` The clang-format major version if specified, 0 -# otherwise ``ClangFormat_VERSION_MINOR`` The clang-format minor version if -# specified, 0 otherwise ``ClangFormat_VERSION_PATCH`` The clang-format patch -# version if specified, 0 otherwise ``ClangFormat_VERSION_COUNT`` Number of -# version components reported by clang-format +# Required version: 12.x +# Docker image fallback: ghcr.io/jidicula/clang-format:12 # -# Example usage: -# -# .. code-block:: cmake -# -# find_package(ClangFormat) if(ClangFormat_FOUND) message("clang-format -# executable found: ${ClangFormat_EXECUTABLE}\n" "version: -# ${ClangFormat_VERSION}") endif() if(ClangFormat_FOUND) set(ClangFormat_FIND_QUIETLY TRUE) endif() +set(CLANG_FORMAT_REQUIRED_MAJOR 12) +set(CLANG_FORMAT_DOCKER_IMAGE "ghcr.io/jidicula/clang-format:12") + +# --------------------------------------------------------------------------- +# Candidate binary names - versioned name first, then generic +# --------------------------------------------------------------------------- set(ClangFormat_NAMES - clang-format - clang-format-11 clang-format-12 + clang-format ) +# --------------------------------------------------------------------------- +# Platform-specific search paths +# --------------------------------------------------------------------------- set(ClangFormat_PATHS + # Linux - standard LLVM apt packages /usr/bin - /usr/lib/llvm-11/bin /usr/lib/llvm-12/bin + + # macOS - Homebrew on Apple Silicon + /opt/homebrew/opt/llvm@12/bin + + # macOS - Homebrew on Intel + /usr/local/opt/llvm@12/bin + + # Windows - Chocolatey and manual LLVM installer default locations + "C:/Program Files/LLVM/bin" + "C:/ProgramData/chocolatey/bin" + + # Windows - Scoop + "$ENV{USERPROFILE}/scoop/apps/llvm/12.0.1/bin" + + # Windows - winget / per-user install + "$ENV{LOCALAPPDATA}/Programs/LLVM/bin" ) find_program(ClangFormat_EXECUTABLE NAMES ${ClangFormat_NAMES} - DOC "clang-format executable" + DOC "clang-format 12 executable" PATHS ${ClangFormat_PATHS} + NO_DEFAULT_PATH ) -# Extract version from command "clang-format -version" +# Also try the system PATH as a last resort before we reach Docker +if(NOT ClangFormat_EXECUTABLE) + find_program(ClangFormat_EXECUTABLE + NAMES ${ClangFormat_NAMES} + DOC "clang-format 12 executable (system PATH)" + ) +endif() + +# --------------------------------------------------------------------------- +# Version extraction and strict major-version validation +# --------------------------------------------------------------------------- if(ClangFormat_EXECUTABLE) - execute_process(COMMAND ${ClangFormat_EXECUTABLE} -version - OUTPUT_VARIABLE clang_format_version - ERROR_QUIET OUTPUT_STRIP_TRAILING_WHITESPACE) - - if(clang_format_version MATCHES "clang-format version .*") - # clang_format_version sample: "clang-format version 3.9.1-4ubuntu3~16.04.1 - # (tags/RELEASE_391/rc2)" - string(REGEX - REPLACE ".*clang-format version ([.0-9]+).*" - "\\1" - ClangFormat_VERSION - "${clang_format_version}") - # ClangFormat_VERSION sample: "3.9.1" - - # Extract version components - string(REPLACE "." ";" clang_format_version "${ClangFormat_VERSION}") - list(LENGTH clang_format_version ClangFormat_VERSION_COUNT) + execute_process( + COMMAND "${ClangFormat_EXECUTABLE}" -version + OUTPUT_VARIABLE _cf_version_out + ERROR_QUIET + OUTPUT_STRIP_TRAILING_WHITESPACE + ) + + if(_cf_version_out MATCHES "clang-format version ([0-9]+\\.[0-9]+[^ ]*)") + set(ClangFormat_VERSION "${CMAKE_MATCH_1}") + + string(REPLACE "." ";" _cf_version_list "${ClangFormat_VERSION}") + list(LENGTH _cf_version_list ClangFormat_VERSION_COUNT) + if(ClangFormat_VERSION_COUNT GREATER 0) - list(GET clang_format_version 0 ClangFormat_VERSION_MAJOR) + list(GET _cf_version_list 0 ClangFormat_VERSION_MAJOR) else() set(ClangFormat_VERSION_MAJOR 0) endif() + if(ClangFormat_VERSION_COUNT GREATER 1) - list(GET clang_format_version 1 ClangFormat_VERSION_MINOR) + list(GET _cf_version_list 1 ClangFormat_VERSION_MINOR) else() set(ClangFormat_VERSION_MINOR 0) endif() + if(ClangFormat_VERSION_COUNT GREATER 2) - list(GET clang_format_version 2 ClangFormat_VERSION_PATCH) + list(GET _cf_version_list 2 ClangFormat_VERSION_PATCH) else() set(ClangFormat_VERSION_PATCH 0) endif() endif() - unset(clang_format_version) + + unset(_cf_version_out) + unset(_cf_version_list) + + # Reject any binary that is not version 12.x + if(NOT ClangFormat_VERSION_MAJOR EQUAL ${CLANG_FORMAT_REQUIRED_MAJOR}) + if(NOT ClangFormat_FIND_QUIETLY) + message(STATUS + "Found clang-format ${ClangFormat_VERSION} at ${ClangFormat_EXECUTABLE}, " + "but version ${CLANG_FORMAT_REQUIRED_MAJOR}.x is required - ignoring." + ) + endif() + unset(ClangFormat_EXECUTABLE CACHE) + unset(ClangFormat_VERSION) + unset(ClangFormat_VERSION_MAJOR) + unset(ClangFormat_VERSION_MINOR) + unset(ClangFormat_VERSION_PATCH) + unset(ClangFormat_VERSION_COUNT) + endif() +endif() + +# --------------------------------------------------------------------------- +# Docker fallback - only on non-Windows platforms +# --------------------------------------------------------------------------- +if(NOT ClangFormat_EXECUTABLE AND NOT WIN32) + find_program(_docker_exe NAMES docker) + + if(_docker_exe) + if(NOT ClangFormat_FIND_QUIETLY) + message(STATUS + "clang-format 12 not found natively. " + "Generating Docker wrapper: ${CLANG_FORMAT_DOCKER_IMAGE}" + ) + endif() + + # The wrapper mounts CMAKE_SOURCE_DIR at the same absolute path + set(_wrapper_path "${CMAKE_BINARY_DIR}/clang-format-docker-wrapper.sh") + + file(WRITE "${_wrapper_path}" +"#!/bin/sh +# Auto-generated by FindClangFormat.cmake - DO NOT EDIT. +# Proxies clang-format calls through the pinned Docker image. +set -e +exec docker run --rm \\ + -v \"${CMAKE_SOURCE_DIR}:${CMAKE_SOURCE_DIR}\" \\ + -w \"${CMAKE_SOURCE_DIR}\" \\ + \"${CLANG_FORMAT_DOCKER_IMAGE}\" \\ + \"\$@\" +" + ) + + execute_process(COMMAND chmod +x "${_wrapper_path}") + + set(ClangFormat_EXECUTABLE "${_wrapper_path}" CACHE FILEPATH "clang-format Docker wrapper" FORCE) + set(ClangFormat_VERSION "12.0.0") + set(ClangFormat_VERSION_MAJOR 12) + set(ClangFormat_VERSION_MINOR 0) + set(ClangFormat_VERSION_PATCH 0) + set(ClangFormat_VERSION_COUNT 3) + set(ClangFormat_USE_DOCKER TRUE CACHE BOOL "clang-format is running via Docker" FORCE) + else() + if(NOT ClangFormat_FIND_QUIETLY) + message(STATUS + "clang-format 12 not found and Docker is not available. " + "Install clang-format-12 or Docker to enable formatting." + ) + endif() + endif() + + unset(_docker_exe CACHE) + unset(_docker_exe) endif() +# --------------------------------------------------------------------------- +# Windows Docker fallback - Docker Desktop must be on PATH +# --------------------------------------------------------------------------- +if(NOT ClangFormat_EXECUTABLE AND WIN32) + find_program(_docker_exe NAMES docker) + + if(_docker_exe) + if(NOT ClangFormat_FIND_QUIETLY) + message(STATUS + "clang-format 12 not found on Windows. " + "Generating Docker wrapper (requires Docker Desktop): ${CLANG_FORMAT_DOCKER_IMAGE}" + ) + endif() + + set(_wrapper_path "${CMAKE_BINARY_DIR}/clang-format-docker-wrapper.cmd") + + # Windows batch wrapper + string(REPLACE "\\" "/" _src_fwd "${CMAKE_SOURCE_DIR}") + + file(WRITE "${_wrapper_path}" +"@echo off +REM Auto-generated by FindClangFormat.cmake -- DO NOT EDIT. +docker run --rm ^ + -v \"${_src_fwd}:${_src_fwd}\" ^ + -w \"${_src_fwd}\" ^ + \"${CLANG_FORMAT_DOCKER_IMAGE}\" ^ + %* +" + ) + + set(ClangFormat_EXECUTABLE "${_wrapper_path}" CACHE FILEPATH "clang-format Docker wrapper (Windows)" FORCE) + set(ClangFormat_VERSION "12.0.0") + set(ClangFormat_VERSION_MAJOR 12) + set(ClangFormat_VERSION_MINOR 0) + set(ClangFormat_VERSION_PATCH 0) + set(ClangFormat_VERSION_COUNT 3) + set(ClangFormat_USE_DOCKER TRUE CACHE BOOL "clang-format is running via Docker" FORCE) + + unset(_src_fwd) + else() + if(NOT ClangFormat_FIND_QUIETLY) + message(WARNING + "clang-format 12 not found and Docker Desktop is not available. " + "Install LLVM 12 from https://github.com/llvm/llvm-project/releases/tag/llvmorg-12.0.1 " + "or install Docker Desktop to enable formatting." + ) + endif() + endif() + + unset(_docker_exe CACHE) + unset(_docker_exe) +endif() + +# --------------------------------------------------------------------------- +# Standard CMake result handling +# --------------------------------------------------------------------------- if(ClangFormat_EXECUTABLE AND ClangFormat_VERSION) set(ClangFormat_FOUND TRUE) include(FindPackageHandleStandardArgs) - # Set standard args find_package_handle_standard_args(ClangFormat REQUIRED_VARS ClangFormat_EXECUTABLE VERSION_VAR ClangFormat_VERSION ) mark_as_advanced(ClangFormat_EXECUTABLE) + + if(NOT ClangFormat_FIND_QUIETLY) + if(ClangFormat_USE_DOCKER) + message(STATUS "clang-format: using Docker image ${CLANG_FORMAT_DOCKER_IMAGE} via wrapper ${ClangFormat_EXECUTABLE}") + else() + message(STATUS "clang-format ${ClangFormat_VERSION} found: ${ClangFormat_EXECUTABLE}") + endif() + endif() else() set(ClangFormat_FOUND FALSE) endif() diff --git a/cmake/SecurityFlags.cmake b/cmake/SecurityFlags.cmake index 3e9a7ced45..6c6599e7a6 100644 --- a/cmake/SecurityFlags.cmake +++ b/cmake/SecurityFlags.cmake @@ -43,7 +43,7 @@ if("${CMAKE_C_COMPILER_ID}" STREQUAL "GNU") set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} --param ssp-buffer-size=4") endif() else() - check_c_compiler_flag_stack_smashing("-fstack-protector" STACK_PROTECTOR_CXX_FLAG) + check_c_compiler_flag_stack_smashing("-fstack-protector" STACK_PROTECTOR_C_FLAG) if(STACK_PROTECTOR_C_FLAG) set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -fstack-protector") @@ -59,7 +59,10 @@ if("${CMAKE_C_COMPILER_ID}" STREQUAL "GNU") check_c_compiler_flag("-D_FORTIFY_SOURCE=2" FORTIFY_SOURCE_C_FLAG) if(FORTIFY_SOURCE_C_FLAG) - set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -O3 -D_FORTIFY_SOURCE=2") + # _FORTIFY_SOURCE=2 requires at least -O1; append only to release + # flag sets so Debug builds are not silently compiled at -O3. + string(APPEND CMAKE_C_FLAGS_RELEASE " -D_FORTIFY_SOURCE=2") + string(APPEND CMAKE_C_FLAGS_RELWITHDEBINFO " -D_FORTIFY_SOURCE=2") endif() endif() @@ -107,7 +110,10 @@ if("${CMAKE_CXX_COMPILER_ID}" STREQUAL "GNU") check_cxx_compiler_flag("-D_FORTIFY_SOURCE=2" FORTIFY_SOURCE_CXX_FLAG) if(FORTIFY_SOURCE_CXX_FLAG) - set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -O3 -D_FORTIFY_SOURCE=2") + # _FORTIFY_SOURCE=2 requires at least -O1; append only to release + # flag sets so Debug builds are not silently compiled at -O3. + string(APPEND CMAKE_CXX_FLAGS_RELEASE " -D_FORTIFY_SOURCE=2") + string(APPEND CMAKE_CXX_FLAGS_RELWITHDEBINFO " -D_FORTIFY_SOURCE=2") endif() endif() diff --git a/cmake/Warnings.cmake b/cmake/Warnings.cmake index c5d42e1c91..cab14280f1 100644 --- a/cmake/Warnings.cmake +++ b/cmake/Warnings.cmake @@ -87,14 +87,14 @@ if(WARNINGS_ENABLED) string(REPLACE "/W2" "" CMAKE_C_FLAGS "${CMAKE_C_FLAGS}") string(REPLACE "/W3" "" CMAKE_C_FLAGS "${CMAKE_C_FLAGS}") set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} /W4") # /Wall - set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} /D _CTR_NONSTDC_NO_WARNINGS=1") - set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} /D _CTR_SECURE_NO_WARNINGS=1") + set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} /D _CRT_NONSTDC_NO_WARNINGS=1") + set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} /D _CRT_SECURE_NO_WARNINGS=1") set(WARNINGS_C_AVAILABLE 1) elseif("${CMAKE_C_COMPILER_ID}" STREQUAL "Intel") set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} /W5 /Wall /Wcheck /Werror-all") set(WARNINGS_C_AVAILABLE 1) else() - set(STATUS "Unknown C compiler warning level support") + message(STATUS "Unknown C compiler warning level support") set(WARNINGS_C_AVAILABLE 0) endif() @@ -107,16 +107,16 @@ if(WARNINGS_ENABLED) string(REPLACE "/W2" "" CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS}") string(REPLACE "/W3" "" CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS}") set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /W4") # /Wall - set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /D _CTR_SECURE_CPP_OVERLOAD_STANDARD_NAMES=1") - set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /D _CTR_SECURE_CPP_OVERLOAD_STANDARD_NAMES_COUNT=1") - set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /D _CTR_NONSTDC_NO_WARNINGS=1") - set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /D _CTR_SECURE_NO_WARNINGS=1") + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /D _CRT_SECURE_CPP_OVERLOAD_STANDARD_NAMES=1") + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /D _CRT_SECURE_CPP_OVERLOAD_STANDARD_NAMES_COUNT=1") + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /D _CRT_NONSTDC_NO_WARNINGS=1") + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /D _CRT_SECURE_NO_WARNINGS=1") set(WARNINGS_CXX_AVAILABLE 1) elseif("${CMAKE_CXX_COMPILER_ID}" STREQUAL "Intel") set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /W5 /Wall /Wcheck /Werror-all") set(WARNINGS_CXX_AVAILABLE 1) else() - set(STATUS "Unknown C++ compiler warning level support") + message(STATUS "Unknown C++ compiler warning level support") set(WARNINGS_CXX_AVAILABLE 0) endif() diff --git a/githooks/unix/pre-commit-clang-format b/githooks/unix/pre-commit-clang-format index 7c76ce8e2b..a9a182a63f 100644 --- a/githooks/unix/pre-commit-clang-format +++ b/githooks/unix/pre-commit-clang-format @@ -4,253 +4,350 @@ # Features: # - abort commit when commit does not comply with the style guidelines # - create a patch of the proposed style changes -# Modifications for clang-format by rene.milk@wwu.de - -# This file is part of a set of unofficial pre-commit hooks available -# at github. -# Link: https://github.com/githubbrowser/Pre-commit-hooks -# Contact: David Martin, david.martin.mailbox@googlemail.com - -# Some quality of life modifications made for Godot Engine. - -# Some modifs for Metacall +# - falls back to Docker (ghcr.io/jidicula/clang-format:12) when clang-format-12 +# is not available natively +# +# Original: https://github.com/githubbrowser/Pre-commit-hooks +# Modified for Godot Engine, then MetaCall. ################################################################## # SETTINGS -# Set path to clang-format binary. -# To get consistent formatting, we recommend contributors to use the same -# clang-format version as CI. -RECOMMENDED_CLANG_FORMAT_MAJOR="11" +# Exact major version required for consistent formatting across all contributors. +REQUIRED_CLANG_FORMAT_MAJOR="12" -function find_clang_format() { - for version in 11 12 13 14 15; do - program=`which clang-format-$version 2>/dev/null` - if [ ! -z "$program" ]; then - echo "$program" - return - fi - done +# Docker image used as fallback when the native binary is not available. +CLANG_FORMAT_DOCKER_IMAGE="ghcr.io/jidicula/clang-format:12" - program=`which clang-format 2>/dev/null` +# Remove any older patches from previous commits. Set to true or false. +DELETE_OLD_PATCHES=false - if [ ! -z "$program" ]; then - version="$($program --version | rev | cut -d' ' -f1 | rev)" - major="$(echo "$version" | cut -d'.' -f1)" +# Only parse files with the extensions in FILE_EXTS. Set to true or false. +PARSE_EXTS=true - if [ "$major" -ge "$RECOMMENDED_CLANG_FORMAT_MAJOR" ]; then +# File types to parse. Only effective when PARSE_EXTS is true. +FILE_EXTS=".c .h .cpp .hpp .cc .hh .cxx" + +################################################################## +# BINARY DISCOVERY + +# Finds clang-format-12 natively. Sets: +# CLANG_FORMAT - path to native binary (empty if not found) +# CLANG_FORMAT_DOCKER - "1" if Docker fallback should be used instead +CLANG_FORMAT="" +CLANG_FORMAT_DOCKER="0" + +function find_native_clang_format() { + # Prefer the explicitly versioned binary. + local program + program=$(which clang-format-${REQUIRED_CLANG_FORMAT_MAJOR} 2>/dev/null) + if [ -n "$program" ]; then + echo "$program" + return 0 + fi + + # Accept the unversioned binary only when it is exactly version 12.x. + program=$(which clang-format 2>/dev/null) + if [ -n "$program" ]; then + local version major + version=$("$program" --version 2>/dev/null | grep -oE '[0-9]+\.[0-9]+(\.[0-9]+)?' | head -1) + major=$(echo "$version" | cut -d'.' -f1) + if [ "$major" = "$REQUIRED_CLANG_FORMAT_MAJOR" ]; then echo "$program" - return + return 0 fi fi + + # macOS Homebrew llvm@12 (Apple Silicon and Intel paths) + for brew_path in \ + "/opt/homebrew/opt/llvm@12/bin/clang-format" \ + "/usr/local/opt/llvm@12/bin/clang-format" + do + if [ -x "$brew_path" ]; then + echo "$brew_path" + return 0 + fi + done + + return 1 } -CLANG_FORMAT=$(find_clang_format) +CLANG_FORMAT=$(find_native_clang_format) -# Remove any older patches from previous commits. Set to true or false. -DELETE_OLD_PATCHES=false +# If native lookup failed, try Docker. +if [ -z "$CLANG_FORMAT" ]; then + if command -v docker >/dev/null 2>&1; then + CLANG_FORMAT_DOCKER="1" + fi +fi -# Only parse files with the extensions in FILE_EXTS. Set to true or false. -# If false every changed file in the commit will be parsed with clang-format. -# If true only files matching one of the extensions are parsed with clang-format. -PARSE_EXTS=true +################################################################## +# FORMATTING HELPERS + +# Returns the repository root (used for Docker volume mounts). +REPO_ROOT=$(git rev-parse --show-toplevel 2>/dev/null) + +# run_clang_format FILE +# Runs clang-format -style=file on FILE and prints the result to stdout. +# Does NOT modify the file in-place (we use the output to build a diff patch). +function run_clang_format() { + local file="$1" + + if [ "$CLANG_FORMAT_DOCKER" = "1" ]; then + # Mount the repository root at the same absolute path so that + # relative file paths remain valid inside the container. + docker run --rm \ + -v "${REPO_ROOT}:${REPO_ROOT}" \ + -w "${REPO_ROOT}" \ + "${CLANG_FORMAT_DOCKER_IMAGE}" \ + -style=file \ + "${file}" + else + "$CLANG_FORMAT" -style=file "${file}" + fi +} -# File types to parse. Only effective when PARSE_EXTS is true. -FILE_EXTS=".c .h .cpp .hpp .cc .hh .cxx" +################################################################## +# OPTIONAL DISPLAY HELPERS -# Use pygmentize instead of cat to parse diff with highlighting. -# Install it with `pip install pygments` (Linux) or `easy_install Pygments` (Mac) -PYGMENTIZE=`which pygmentize 2>/dev/null` -if [ ! -z "$PYGMENTIZE" ]; then - READER="pygmentize -l diff" +# Use pygmentize instead of cat to highlight the diff, if available. +PYGMENTIZE=$(which pygmentize 2>/dev/null) +if [ -n "$PYGMENTIZE" ]; then + READER="pygmentize -l diff" else - READER=cat + READER=cat fi -# Path to zenity -ZENITY=`which zenity 2>/dev/null` - -# Path to xmessage -XMSG=`which xmessage 2>/dev/null` - -# Path to powershell (Windows only) -PWSH=`which powershell 2>/dev/null` +# GUI dialogs (for non-terminal environments) +ZENITY=$(which zenity 2>/dev/null) +XMSG=$(which xmessage 2>/dev/null) +PWSH=$(which powershell 2>/dev/null) ################################################################## -# There should be no need to change anything below this line. +# BOOTSTRAP . "$(dirname -- "$0")/canonicalize_filename.sh" -# exit on error +# Exit on error - but only before we enter the interactive prompt section. set -e +################################################################## +# PREFLIGHT CHECKS + # check whether the given file matches any of the set extensions matches_extension() { - local filename=$(basename "$1") + local filename + filename=$(basename "$1") local extension=".${filename##*.}" local ext - for ext in $FILE_EXTS; do [[ "$ext" == "$extension" ]] && return 0; done - + for ext in $FILE_EXTS; do + [[ "$ext" == "$extension" ]] && return 0 + done return 1 } -# necessary check for initial commit -if git rev-parse --verify HEAD >/dev/null 2>&1 ; then +# Determine what we diff against: HEAD for existing repos, empty-tree for the +# very first commit. +if git rev-parse --verify HEAD >/dev/null 2>&1; then against=HEAD else - # Initial commit: diff against an empty tree object + # Empty tree SHA - stable across all Git versions. against=4b825dc642cb6eb9a060e54bf8d69288fbee4904 fi -if [ ! -x "$CLANG_FORMAT" ] ; then - message="Error: clang-format executable not found. Please install clang-format $RECOMMENDED_CLANG_FORMAT_MAJOR.x.x. or superior" +# Abort early with a clear message when neither native binary nor Docker is +# available so the developer knows exactly what to do. +if [ -z "$CLANG_FORMAT" ] && [ "$CLANG_FORMAT_DOCKER" = "0" ]; then + message="Error: clang-format ${REQUIRED_CLANG_FORMAT_MAJOR}.x not found and Docker is not available. + +To fix this, choose one of the following options: + + Linux (Debian/Ubuntu): + wget -qO- https://apt.llvm.org/llvm-snapshot.gpg.key | sudo tee /etc/apt/trusted.gpg.d/apt.llvm.org.asc + sudo add-apt-repository 'deb http://apt.llvm.org/\$(lsb_release -cs)/ llvm-toolchain-\$(lsb_release -cs)-12 main' + sudo apt-get update && sudo apt-get install -y clang-format-12 + + Linux (Alpine): + apk add --no-cache --repository=https://dl-cdn.alpinelinux.org/alpine/v3.15/main clang-extra-tools=12.0.1-r1 + + macOS: + brew install llvm@12 + export PATH=\"\$(brew --prefix llvm@12)/bin:\$PATH\" + + Windows: + Download the LLVM 12.0.1 installer from: + https://github.com/llvm/llvm-project/releases/tag/llvmorg-12.0.1 + + Any platform (Docker): + Install Docker and the hook will use ghcr.io/jidicula/clang-format:12 automatically." - if [ ! -t 1 ] ; then - if [ -x "$ZENITY" ] ; then - $ZENITY --error --title="Error" --text="$message" + if [ ! -t 1 ]; then + if [ -x "$ZENITY" ]; then + $ZENITY --error --title="clang-format not found" --text="$message" exit 1 - elif [ -x "$XMSG" ] ; then - $XMSG -center -title "Error" "$message" + elif [ -x "$XMSG" ]; then + $XMSG -center -title "clang-format not found" "$message" exit 1 - elif [ \( \( "$OSTYPE" = "msys" \) -o \( "$OSTYPE" = "win32" \) \) -a \( -x "$PWSH" \) ]; then + elif [ \( "$OSTYPE" = "msys" -o "$OSTYPE" = "win32" \) ] && [ -x "$PWSH" ]; then winmessage="$(canonicalize_filename "./.git/hooks/winmessage.ps1")" - $PWSH -noprofile -executionpolicy bypass -file "$winmessage" -center -title "Error" --text "$message" + $PWSH -noprofile -executionpolicy bypass -file "$winmessage" \ + -center -title "clang-format not found" --text "$message" exit 1 fi fi - printf "$message\n" - printf "Set the correct path in $(canonicalize_filename "$0").\n" + + printf '%s\n' "$message" exit 1 fi -# create a random filename to store our generated patch +# Show which backend is active so contributors are never surprised. +if [ "$CLANG_FORMAT_DOCKER" = "1" ]; then + printf "clang-format: using Docker image %s\n" "$CLANG_FORMAT_DOCKER_IMAGE" + + # Pull the image eagerly so the hook doesn't silently stall + docker pull "$CLANG_FORMAT_DOCKER_IMAGE" >/dev/null 2>&1 || true +else + printf "clang-format: using %s\n" "$CLANG_FORMAT" +fi + +################################################################## +# PATCH GENERATION + prefix="pre-commit-clang-format" suffix="$(date +%s)" -patch="/tmp/$prefix-$suffix.patch" - -# clean up any older clang-format patches -$DELETE_OLD_PATCHES && rm -f /tmp/$prefix*.patch - -# create one patch containing all changes to the files -git diff-index --cached --diff-filter=ACMR --name-only $against -- | while read file; -do - # ignore thirdparty folder - #if grep -q "thirdparty" <<< $file; then - # continue; - #fi - - # Only files from source folder - if grep -q "source" <<< $file; then - : # Correct - else - continue; - fi +patch="/tmp/${prefix}-${suffix}.patch" +$DELETE_OLD_PATCHES && rm -f /tmp/${prefix}*.patch - # ignore file if we do check for file extensions and the file - # does not match any of the extensions specified in $FILE_EXTS +# Iterate over every staged C/C++ file under source/ and build a unified diff +# patch that clang-format would apply. +git diff-index --cached --diff-filter=ACMR --name-only "$against" -- \ +| while read -r file; do + + # Only files from the source/ directory. + if ! grep -q "source" <<< "$file"; then + continue + fi + + # Extension filter. if $PARSE_EXTS && ! matches_extension "$file"; then - continue; + continue fi - # clang-format our sourcefile, create a patch with diff and append it to our $patch - # The sed call is necessary to transform the patch from - # --- $file timestamp - # +++ - timestamp - # to both lines working on the same file and having a/ and b/ prefix. - # Else it can not be applied with 'git apply'. - "$CLANG_FORMAT" -style=file "$file" | \ - diff -u "$file" - | \ - sed -e "1s|--- |--- a/|" -e "2s|+++ -|+++ b/$file|" >> "$patch" + # Format the file and diff the result. + # sed rewrites the patch headers to the `a/` `b/` form that `git apply` + # requires. + run_clang_format "$file" \ + | diff -u "$file" - \ + | sed -e "1s|--- |--- a/|" -e "2s|+++ -|+++ b/$file|" \ + >> "$patch" \ + || true # diff exits 1 when files differ; that is expected done -# if no patch has been generated all is ok, clean up the file stub and exit -if [ ! -s "$patch" ] ; then +################################################################## +# RESULT + +# No patch → everything already conforms. +if [ ! -s "$patch" ]; then printf "Files in this commit comply with the clang-format rules.\n" rm -f "$patch" exit 0 fi -# a patch has been created, notify the user and exit +# A patch exists - show the diff and ask the developer what to do. printf "\nThe following differences were found between the code to commit " printf "and the clang-format rules:\n\n" -if [ -t 1 ] ; then +# Disable exit-on-error for the interactive section. +set +e + +if [ -t 1 ]; then $READER "$patch" printf "\n" - # Allows us to read user input below, assigns stdin to keyboard exec < /dev/tty terminal="1" else cat "$patch" printf "\n" - # Allows non zero zenity/powershell output - set +e terminal="0" fi while true; do - if [ $terminal = "0" ] ; then - if [ -x "$ZENITY" ] ; then - ans=$($ZENITY --text-info --filename="$patch" --width=800 --height=600 --title="Do you want to apply that patch?" --ok-label="Apply" --cancel-label="Do not apply" --extra-button="Apply and stage") - if [ "$?" = "0" ] ; then - yn="Y" - else - if [ "$ans" = "Apply and stage" ] ; then - yn="S" - else - yn="N" - fi - fi - elif [ -x "$XMSG" ] ; then - $XMSG -file "$patch" -buttons "Apply":100,"Apply and stage":200,"Do not apply":0 -center -default "Do not apply" -geometry 800x600 -title "Do you want to apply that patch?" - ans=$? - if [ "$ans" = "100" ] ; then + if [ "$terminal" = "0" ]; then + if [ -x "$ZENITY" ]; then + ans=$($ZENITY \ + --text-info \ + --filename="$patch" \ + --width=800 --height=600 \ + --title="Do you want to apply that patch?" \ + --ok-label="Apply" \ + --cancel-label="Do not apply" \ + --extra-button="Apply and stage") + if [ "$?" = "0" ]; then yn="Y" - elif [ "$ans" = "200" ] ; then + elif [ "$ans" = "Apply and stage" ]; then yn="S" else yn="N" fi - elif [ \( \( "$OSTYPE" = "msys" \) -o \( "$OSTYPE" = "win32" \) \) -a \( -x "$PWSH" \) ]; then + elif [ -x "$XMSG" ]; then + $XMSG -file "$patch" \ + -buttons "Apply":100,"Apply and stage":200,"Do not apply":0 \ + -center -default "Do not apply" \ + -geometry 800x600 \ + -title "Do you want to apply that patch?" + ans=$? + if [ "$ans" = "100" ]; then yn="Y" + elif [ "$ans" = "200" ]; then yn="S" + else yn="N" + fi + elif [ \( "$OSTYPE" = "msys" -o "$OSTYPE" = "win32" \) ] && [ -x "$PWSH" ]; then winmessage="$(canonicalize_filename "./.git/hooks/winmessage.ps1")" - $PWSH -noprofile -executionpolicy bypass -file "$winmessage" -file "$patch" -buttons "Apply":100,"Apply and stage":200,"Do not apply":0 -center -default "Do not apply" -geometry 800x600 -title "Do you want to apply that patch?" + $PWSH -noprofile -executionpolicy bypass -file "$winmessage" \ + -file "$patch" \ + -buttons "Apply":100,"Apply and stage":200,"Do not apply":0 \ + -center -default "Do not apply" \ + -geometry 800x600 \ + -title "Do you want to apply that patch?" ans=$? - if [ "$ans" = "100" ] ; then - yn="Y" - elif [ "$ans" = "200" ] ; then - yn="S" - else - yn="N" + if [ "$ans" = "100" ]; then yn="Y" + elif [ "$ans" = "200" ]; then yn="S" + else yn="N" fi else - printf "Error: zenity, xmessage, or powershell executable not found.\n" + printf "Error: zenity, xmessage, or powershell not found (non-terminal mode).\n" exit 1 fi else - read -p "Do you want to apply that patch (Y - Apply, N - Do not apply, S - Apply and stage files)? [Y/N/S] " yn + read -r -p "Apply patch? [Y = apply, S = apply & stage, N = abort] " yn fi - case $yn in - [Yy] ) git apply $patch; - printf "The patch was applied. You can now stage the changes and commit again.\n\n"; - break - ;; - [Nn] ) printf "\nYou can apply these changes with:\n git apply $patch\n"; - printf "(may need to be called from the root directory of your repository)\n"; - printf "Aborting commit. Apply changes and commit again or skip checking with"; - printf " --no-verify (not recommended).\n\n"; - break - ;; - [Ss] ) git apply $patch; - git diff-index --cached --diff-filter=ACMR --name-only $against -- | while read file; - do git add $file; - done - printf "The patch was applied and the changed files staged. You can now commit.\n\n"; - break - ;; - * ) echo "Please answer yes or no." - ;; + + case "$yn" in + [Yy]) + git apply "$patch" + printf "Patch applied. Stage the changes and commit again.\n\n" + break + ;; + [Ss]) + git apply "$patch" + git diff-index --cached --diff-filter=ACMR --name-only "$against" -- \ + | while read -r file; do + git add "$file" + done + printf "Patch applied and files staged. You can now commit.\n\n" + break + ;; + [Nn]) + printf "\nYou can apply these changes manually with:\n git apply %s\n" "$patch" + printf "(run from the root of the repository)\n" + printf "Aborting commit. Apply the patch and commit again, or skip with --no-verify.\n\n" + break + ;; + *) + echo "Please answer Y, N, or S." + ;; esac done -exit 1 # we don't commit in any case + +exit 1 # The commit is never finalised automatically - always re-commit after fixing. diff --git a/tools/metacall-build.sh b/tools/metacall-build.sh index f5cb079bdf..f9ee3c1aac 100755 --- a/tools/metacall-build.sh +++ b/tools/metacall-build.sh @@ -74,7 +74,7 @@ sub_build() { make -j$(getconf _NPROCESSORS_ONLN) # Tests (coverage needs to run the tests) - if [ $BUILD_TESTS = 1 ] || [ $BUILD_BENCHMARKS=1 ] || [ $BUILD_COVERAGE = 1 ]; then + if [ $BUILD_TESTS = 1 ] || [ $BUILD_BENCHMARKS = 1 ] || [ $BUILD_COVERAGE = 1 ]; then ctest -j$(getconf _NPROCESSORS_ONLN) --timeout 5400 --output-on-failure --test-output-size-failed 3221000000 -C $BUILD_TYPE fi diff --git a/tools/metacall-environment.ps1 b/tools/metacall-environment.ps1 index 18ba1f6cf8..aedaab6808 100755 --- a/tools/metacall-environment.ps1 +++ b/tools/metacall-environment.ps1 @@ -243,6 +243,99 @@ function Set-Curl { Write-Output "-DCURL_LIBRARY_NAME=""$CURL_LIB_NAME""" >> $EnvOpts } +# Install clang-format 12 on Windows. +# Strategy: +# 1. Download the standalone clang-format.exe from the LLVM 12.0.1 GitHub release. +# 2. Place it in a dedicated directory and add that directory to PATH. +# 3. Also copy it as clang-format-12.exe so cmake/FindClangFormat.cmake finds it +# by its versioned name. +# 4. If Docker Desktop is available and the download fails, install a wrapper +# batch file that proxies calls through ghcr.io/jidicula/clang-format:12. +function Set-ClangFormat { + Write-Output "Installing clang-format 12" + + Set-Location $ROOT_DIR + + $ClangFormatVersion = "12.0.1" + $ClangFormatDir = "$env:ProgramFiles\clang-format-12" + $DepsDir = "$ROOT_DIR\dependencies" + + # LLVM publishes standalone clang-format binaries on GitHub Releases. + $DownloadUrl = "https://github.com/llvm/llvm-project/releases/download/llvmorg-$ClangFormatVersion/clang-format-$ClangFormatVersion.exe" + + mkdir -Force $DepsDir | Out-Null + mkdir -Force $ClangFormatDir | Out-Null + + $ExeDest = "$DepsDir\clang-format-$ClangFormatVersion.exe" + + $NativeInstalled = $false + + # --- Native binary --- + if (!(Test-Path -Path $ExeDest)) { + Write-Output "Downloading clang-format $ClangFormatVersion from LLVM GitHub releases..." + try { + (New-Object Net.WebClient).DownloadFile($DownloadUrl, $ExeDest) + } catch { + Write-Output "Download failed: $_" + } + } + + if (Test-Path -Path $ExeDest) { + # Expose as both the versioned and the unversioned name. + Copy-Item -Force $ExeDest "$ClangFormatDir\clang-format-12.exe" + Copy-Item -Force $ExeDest "$ClangFormatDir\clang-format.exe" + + Add-to-Path $ClangFormatDir + + Write-Output "clang-format 12 installed at $ClangFormatDir" + $NativeInstalled = $true + } else { + Write-Output "Native clang-format binary not available." + } + + # --- Docker Desktop fallback --- + if (-not $NativeInstalled) { + $DockerExe = Get-Command docker -ErrorAction SilentlyContinue + if ($null -ne $DockerExe) { + Write-Output "Docker Desktop detected. Installing Docker wrapper for clang-format 12..." + + $DockerImage = "ghcr.io/jidicula/clang-format:12" + + # Pull the image now so the first invocation is not slow. + docker pull $DockerImage + + # a .cmd wrapper that forwards all arguments into the container. + $WrapperContent = @" +@echo off +REM Auto-generated by metacall-environment.ps1 -- DO NOT EDIT. +REM Proxies clang-format calls through the pinned Docker image. +docker run --rm ^ + -v "%CD%:%CD%" ^ + -w "%CD%" ^ + "$DockerImage" ^ + %* +"@ + + $WrapperDir = "$env:ProgramFiles\clang-format-12" + mkdir -Force $WrapperDir | Out-Null + + $WrapperContent | Out-File -Encoding ascii "$WrapperDir\clang-format-12.cmd" + $WrapperContent | Out-File -Encoding ascii "$WrapperDir\clang-format.cmd" + + Add-to-Path $WrapperDir + + Write-Output "Docker wrapper installed at $WrapperDir" + } else { + Write-Output "WARNING: clang-format 12 could not be installed." + Write-Output "Please install it manually:" + Write-Output " Option A (native): Download from https://github.com/llvm/llvm-project/releases/tag/llvmorg-12.0.1" + Write-Output " Option B (Docker): Install Docker Desktop, then re-run this script." + Write-Output " Option C (winget): winget install LLVM.LLVM --version 12.0.1" + Write-Output " Option D (choco): choco install llvm --version 12.0.1" + } + } +} + function Add-to-Path { $GivenPath = $args[0] @@ -367,6 +460,7 @@ function Configure { } if ("$var" -eq 'clangformat') { Write-Output "clangformat selected" + Set-ClangFormat } } } diff --git a/tools/metacall-environment.sh b/tools/metacall-environment.sh index 0022a1ded1..2f12327a29 100755 --- a/tools/metacall-environment.sh +++ b/tools/metacall-environment.sh @@ -806,9 +806,52 @@ sub_clangformat(){ echo "configure clangformat" cd $ROOT_DIR + CLANG_FORMAT_VERSION=12 + CLANG_FORMAT_DOCKER_IMAGE="ghcr.io/jidicula/clang-format:12" + CLANG_FORMAT_INSTALLED=0 + + # Helper: install a Docker-based wrapper script at /usr/local/bin/clang-format-12 + # so that cmake/FindClangFormat.cmake and the git hook find a callable binary. + install_docker_wrapper(){ + if ! command -v docker >/dev/null 2>&1; then + echo "clang-format-12 could not be installed natively and Docker is not available." + echo "Please install Docker or clang-format-12 manually." + return 1 + fi + + echo "Installing Docker-based clang-format-${CLANG_FORMAT_VERSION} wrapper..." + + # Pull the image eagerly so first use is not slow. + docker pull "${CLANG_FORMAT_DOCKER_IMAGE}" || true + + # a thin wrapper that forwards all arguments into the container. + $SUDO_CMD tee /usr/local/bin/clang-format-${CLANG_FORMAT_VERSION} > /dev/null << 'DOCKER_WRAPPER' +#!/bin/sh +# Auto-generated by metacall-environment.sh - DO NOT EDIT. +# Proxies clang-format calls through the pinned Docker image. +set -e +exec docker run --rm \ + -v "$(pwd):$(pwd)" \ + -w "$(pwd)" \ + "ghcr.io/jidicula/clang-format:12" \ + "$@" +DOCKER_WRAPPER + + $SUDO_CMD chmod +x /usr/local/bin/clang-format-${CLANG_FORMAT_VERSION} + + # Provide an unversioned alias only when there is no system clang-format + # so we do not shadow a potentially newer binary used for other purposes. + if ! command -v clang-format >/dev/null 2>&1; then + $SUDO_CMD ln -sf /usr/local/bin/clang-format-${CLANG_FORMAT_VERSION} \ + /usr/local/bin/clang-format + fi + + echo "Docker wrapper installed at /usr/local/bin/clang-format-${CLANG_FORMAT_VERSION}" + CLANG_FORMAT_INSTALLED=1 + } + if [ "${OPERATIVE_SYSTEM}" = "Linux" ]; then if [ "${LINUX_DISTRO}" = "debian" ] || [ "${LINUX_DISTRO}" = "ubuntu" ]; then - LLVM_VERSION_STRING=12 UBUNTU_CODENAME="" CODENAME_FROM_ARGUMENTS="" @@ -838,15 +881,111 @@ sub_clangformat(){ ;; esac - wget -qO- https://apt.llvm.org/llvm-snapshot.gpg.key | $SUDO_CMD tee /etc/apt/trusted.gpg.d/apt.llvm.org.asc - $SUDO_CMD sh -c "echo \"deb http://apt.llvm.org/${CODENAME}/ llvm-toolchain${LINKNAME}-${LLVM_VERSION_STRING} main\" >> /etc/apt/sources.list" - $SUDO_CMD sh -c "echo \"deb-src http://apt.llvm.org/${CODENAME}/ llvm-toolchain${LINKNAME}-${LLVM_VERSION_STRING} main\" >> /etc/apt/sources.list" - $SUDO_CMD apt-get update - $SUDO_CMD apt-get install -y --no-install-recommends clang-format-${LLVM_VERSION_STRING} - $SUDO_CMD ln -s /usr/bin/clang-format-${LLVM_VERSION_STRING} /usr/bin/clang-format + # Try the LLVM apt repository first (guaranteed to have clang-format-12). + if wget -qO- https://apt.llvm.org/llvm-snapshot.gpg.key \ + | $SUDO_CMD tee /etc/apt/trusted.gpg.d/apt.llvm.org.asc > /dev/null 2>&1; then + $SUDO_CMD sh -c "echo \"deb http://apt.llvm.org/${CODENAME}/ llvm-toolchain${LINKNAME}-${CLANG_FORMAT_VERSION} main\" >> /etc/apt/sources.list" + $SUDO_CMD sh -c "echo \"deb-src http://apt.llvm.org/${CODENAME}/ llvm-toolchain${LINKNAME}-${CLANG_FORMAT_VERSION} main\" >> /etc/apt/sources.list" + $SUDO_CMD apt-get update + if $SUDO_CMD apt-get install -y --no-install-recommends clang-format-${CLANG_FORMAT_VERSION}; then + # Provide an unversioned alias only when absent. + if [ ! -e /usr/bin/clang-format ]; then + $SUDO_CMD ln -s /usr/bin/clang-format-${CLANG_FORMAT_VERSION} /usr/bin/clang-format + fi + CLANG_FORMAT_INSTALLED=1 + fi + fi + + # Fall back to Docker wrapper when the apt install failed + # (e.g. network error, unsupported codename). + if [ $CLANG_FORMAT_INSTALLED -eq 0 ]; then + echo "Native apt install of clang-format-${CLANG_FORMAT_VERSION} failed, trying Docker wrapper..." + install_docker_wrapper + fi + elif [ "${LINUX_DISTRO}" = "alpine" ]; then - $SUDO_CMD apk add --no-cache --repository=https://dl-cdn.alpinelinux.org/alpine/v3.15/main clang-extra-tools=12.0.1-r1 + # Alpine 3.15 ships clang-extra-tools 12.0.1; + # later versions. Try the pinned version first, fall back to Docker. + if $SUDO_CMD apk add --no-cache \ + --repository=https://dl-cdn.alpinelinux.org/alpine/v3.15/main \ + clang-extra-tools=12.0.1-r1 2>/dev/null; then + CLANG_FORMAT_INSTALLED=1 + else + echo "Alpine apk install of clang-extra-tools=12.0.1-r1 failed, trying Docker wrapper..." + install_docker_wrapper + fi + + elif [ "${LINUX_DISTRO}" = "fedora" ]; then + # Fedora ships clang-tools-extra which includes clang-format. + # The exact version depends on the Fedora release; verify after install. + if $SUDO_CMD dnf install -y clang-tools-extra 2>/dev/null; then + installed_major=$(clang-format --version 2>/dev/null | grep -oE '[0-9]+' | head -1) + if [ "${installed_major}" = "${CLANG_FORMAT_VERSION}" ]; then + CLANG_FORMAT_INSTALLED=1 + else + echo "Fedora provided clang-format ${installed_major}, but ${CLANG_FORMAT_VERSION} is required." + install_docker_wrapper + fi + else + install_docker_wrapper + fi + + elif [ "${LINUX_DISTRO}" = "rhel" ] || [ "${LINUX_DISTRO}" = "centos" ]; then + # RHEL / CentOS Stream - try clang-tools-extra from CodeReady/AppStream. + if $SUDO_CMD yum install -y clang-tools-extra 2>/dev/null || \ + $SUDO_CMD dnf install -y clang-tools-extra 2>/dev/null; then + installed_major=$(clang-format --version 2>/dev/null | grep -oE '[0-9]+' | head -1) + if [ "${installed_major}" = "${CLANG_FORMAT_VERSION}" ]; then + CLANG_FORMAT_INSTALLED=1 + else + echo "System provided clang-format ${installed_major}, but ${CLANG_FORMAT_VERSION} is required." + install_docker_wrapper + fi + else + install_docker_wrapper + fi + + elif [ "${LINUX_DISTRO}" = "opensuse" ] || [ "${LINUX_DISTRO}" = "suse" ]; then + if $SUDO_CMD zypper install -y clang12 2>/dev/null; then + CLANG_FORMAT_INSTALLED=1 + else + install_docker_wrapper + fi + + else + # Unknown Linux distribution - go straight to Docker wrapper. + echo "Unrecognised Linux distribution '${LINUX_DISTRO}'. Trying Docker wrapper..." + install_docker_wrapper fi + + elif [ "${OPERATIVE_SYSTEM}" = "Darwin" ]; then + # macOS - Homebrew provides llvm@12 as a versioned formula. + if command -v brew >/dev/null 2>&1; then + brew install llvm@12 + + LLVM12_PREFIX="$(brew --prefix llvm@12)" + + # Expose the versioned binary name that cmake/FindClangFormat.cmake + # and the git hook both search for. + if [ ! -e /usr/local/bin/clang-format-${CLANG_FORMAT_VERSION} ]; then + $SUDO_CMD ln -sf "${LLVM12_PREFIX}/bin/clang-format" \ + /usr/local/bin/clang-format-${CLANG_FORMAT_VERSION} + fi + + # Add llvm@12 bin dir to the current shell session so subsequent + # steps in the same script invocation pick it up immediately. + export PATH="${LLVM12_PREFIX}/bin:${PATH}" + + CLANG_FORMAT_INSTALLED=1 + else + echo "Homebrew not found on macOS. Trying Docker wrapper..." + install_docker_wrapper + fi + fi + + if [ $CLANG_FORMAT_INSTALLED -eq 0 ]; then + echo "Warning: clang-format-${CLANG_FORMAT_VERSION} could not be installed." \ + "Formatting will be unavailable." fi }