diff --git a/.github/workflows/check-cmake.yaml b/.github/workflows/check-cmake.yaml deleted file mode 100644 index 9ab7ef5ec..000000000 --- a/.github/workflows/check-cmake.yaml +++ /dev/null @@ -1,46 +0,0 @@ -name: Run code style checks - -on: - push: - branches: [PHP-8.3] - pull_request: - branches: [PHP-8.3] - -jobs: - check: - name: Check code style - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v4 - with: - fetch-depth: 2 - - - name: Setup Python - uses: actions/setup-python@v5 - - - name: Setup PHP - uses: shivammathur/setup-php@v2 - with: - php-version: 8.4 - tools: php-cs-fixer - - - name: Install checking tools - run: | - pipx install codespell - pipx install gersemi - curl -OL https://github.com/petk/normalizator/releases/latest/download/normalizator.phar - chmod +x normalizator.phar - mv normalizator.phar /usr/local/bin/normalizator - - - name: Run checks - run: ./bin/check-cmake.php - - # TODO: Remove PHP_CS_FIXER_IGNORE_ENV once php-cs-fixer supports PHP 8.4. - - name: Run PHP CS Fixer - run: | - PHP_CS_FIXER_IGNORE_ENV=1 php-cs-fixer check \ - --diff \ - --rules=@Symfony,@PER-CS \ - --using-cache=no \ - -- bin diff --git a/.github/workflows/check.yaml b/.github/workflows/check.yaml new file mode 100644 index 000000000..827027b96 --- /dev/null +++ b/.github/workflows/check.yaml @@ -0,0 +1,54 @@ +name: Run code style checks + +on: + push: + branches: [PHP-8.3] + pull_request: + branches: [PHP-8.3] + +jobs: + check: + name: Check code style + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 2 + + - name: Install CMake + uses: lukka/get-cmake@latest + + - name: Install PHP + uses: shivammathur/setup-php@v2 + with: + php-version: 8.4 + + - name: Install Python + uses: actions/setup-python@v5 + + - name: Install checking tools + run: | + pipx install codespell + curl -OL https://github.com/petk/normalizator/releases/latest/download/normalizator.phar + chmod +x normalizator.phar + mv normalizator.phar /usr/local/bin/normalizator + curl -OL https://github.com/petk/cmake-normalizer/releases/latest/download/normalizer.cmake + chmod +x normalizer.cmake + mv normalizer.cmake /usr/local/bin/normalizer.cmake + + - name: Run Codespell + continue-on-error: true + run: codespell + + - name: Run normalizator.phar + continue-on-error: true + run: normalizator check --not php-src --not .git . + + - name: Run normalizer.cmake + continue-on-error: true + run: | + normalizer.cmake -- \ + --set normalize_indent_style=false \ + --set normalize_cmake_minimum_required=3.25...3.31 \ + -j diff --git a/bin/check-cmake.php b/bin/check-cmake.php deleted file mode 100755 index 1cbc8d30f..000000000 --- a/bin/check-cmake.php +++ /dev/null @@ -1,771 +0,0 @@ -#!/usr/bin/env php -] [] - - OPTIONS: - -h, --help Display this help and exit. - --gersemi Run gersemi. - - FEATURES: - - Reports missing and unused CMake find and utility modules - - Runs codespell, normalizator, and gersemi tools if found - EOL; -} - -/** - * Echo given message and append a newline character. - */ -function output(string $message = ''): void -{ - echo $message . "\n"; -} - -/** - * Get options from the command line arguments. - */ -function options(array $overrides = [], array $argv = []): array -{ - static $options; - - if (isset($options)) { - $options = array_merge($options, $overrides); - - return $options; - } - - $shortOptions = 'h'; - - $longOptions = [ - 'help', - 'gersemi', - ]; - - $optionsIndex = null; - $cliOptions = getopt($shortOptions, $longOptions, $optionsIndex); - - $options = [ - 'help' => false, - 'path' => null, - 'script' => pathinfo($argv[0], PATHINFO_BASENAME), - 'gersemi' => false, - ]; - - foreach ($cliOptions as $option => $value) { - switch ($option) { - case 'h': - case 'help': - $options['help'] = true; - break; - case 'gersemi': - $options['gersemi'] = true; - break; - } - } - - foreach (array_slice($argv, $optionsIndex) as $file) { - if (file_exists($file) && $file !== $argv[0]) { - $options['path'] = realpath($file); - - break; - } - } - - if (array_key_exists('path', $options) && null === $options['path']) { - $options['path'] = realpath(__DIR__ . '/..'); - } - - validateOptions($options); - - return $options; -} - -/** - * Validate command-line options and arguments. - */ -function validateOptions(array $options): void -{ - // Check for help. - if ($options['help']) { - output(usage($options['script'])); - exit(1); - } - - // Check if path exists. - if ( - !array_key_exists('path', $options) - || null === $options['path'] - || !file_exists($options['path']) - ) { - output('Error: Path not found'); - output(); - output('Usage:'); - output(); - output(usage($options['script'])); - exit(1); - } -} - -/** - * Get all project modules. - */ -function getProjectModules(): array -{ - return [ - 'CheckCCompilerFlag' => ['check_c_compiler_flag'], - 'CheckCompilerFlag' => ['check_compiler_flag'], - 'CheckCSourceCompiles' => ['check_c_source_compiles'], - 'CheckCSourceRuns' => ['check_c_source_runs'], - 'CheckCXXCompilerFlag' => ['check_cxx_compiler_flag'], - 'CheckCXXSourceCompiles' => ['check_cxx_source_compiles'], - 'CheckCXXSourceRuns' => ['check_cxx_source_runs'], - 'CheckCXXSymbolExists' => ['check_cxx_symbol_exists'], - 'CheckFunctionExists' => ['check_function_exists'], - 'CheckIncludeFile' => ['check_include_file'], - 'CheckIncludeFileCXX' => ['check_include_file_cxx'], - 'CheckIncludeFiles' => ['check_include_files'], - 'CheckIPOSupported' => ['check_ipo_supported'], - 'CheckLanguage' => ['check_language'], - 'CheckLibraryExists' => ['check_library_exists'], - 'CheckLinkerFlag' => ['check_linker_flag'], - 'CheckPIESupported' => ['check_pie_supported'], - 'CheckPrototypeDefinition' => ['check_prototype_definition'], - 'CheckSourceCompiles' => ['check_source_compiles'], - 'CheckSourceRuns' => ['check_source_runs'], - 'CheckStructHasMember' => ['check_struct_has_member'], - 'CheckSymbolExists' => ['check_symbol_exists'], - 'CheckTypeSize' => ['check_type_size'], - 'CheckVariableExists' => ['check_variable_exists'], - 'CMakeDependentOption' => ['cmake_dependent_option'], - 'CMakeFindDependencyMacro' => ['find_dependency'], - 'CMakePackageConfigHelpers' => [ - 'configure_package_config_file', - 'generate_apple_architecture_selection_file', - 'generate_apple_platform_selection_file', - 'write_basic_package_version_file', - ], - 'CMakePrintHelpers' => [ - 'cmake_print_properties', - 'cmake_print_variables', - ], - 'CMakePushCheckState' => [ - 'cmake_pop_check_state', - 'cmake_push_check_state', - 'cmake_reset_check_state', - ], - 'ExternalProject' => [ - 'ExternalProject_Add', - 'ExternalProject_Add_Step', - 'ExternalProject_Add_StepDependencies', - 'ExternalProject_Add_StepTargets', - 'ExternalProject_Get_Property', - ], - 'FeatureSummary' => [ - 'add_feature_info', - 'feature_summary', - 'set_package_properties', - ], - 'FetchContent' => [ - 'FetchContent_Declare', - 'FetchContent_GetProperties', - 'FetchContent_MakeAvailable', - 'FetchContent_Populate', - 'FetchContent_SetPopulated', - ], - 'FindPackageHandleStandardArgs' => [ - 'find_package_check_version', - 'find_package_handle_standard_args', - ], - 'FindPackageMessage' => ['find_package_message'], - 'GenerateExportHeader' => ['generate_export_header'], - 'ProcessorCount' => ['ProcessorCount'], - 'PHP/AddCustomCommand' => ['php_add_custom_command'], - 'PHP/Bison' => ['php_bison', '/find_package\([\n ]*BISON/'], - 'PHP/CheckAttribute' => [ - 'php_check_function_attribute', - 'php_check_variable_attribute', - ], - 'PHP/CheckCompilerFlag' => ['php_check_compiler_flag'], - 'PHP/ConfigureFile' => ['php_configure_file'], - 'PHP/Install' => ['php_install'], - 'PHP/PkgConfig' => ['php_pkgconfig_generate_pc'], - 'PHP/Re2c' => ['php_re2c', '/find_package\([\n ]*RE2C/'], - 'PHP/SearchLibraries' => ['php_search_libraries'], - 'PHP/Set' => ['php_set'], - 'PHP/SystemExtensions' => ['PHP::SystemExtensions'], - ]; -} - -/** - * Return filtered CMake code without single and multi-line comments. - * - * Hash characters (#) inside quotes (") are not supported until more advanced - * parsing is needed. - */ -function getCMakeCode(SplFileInfo $file): string -{ - $content = file_get_contents($file->getRealPath()); - $content = preg_replace('/\#\[[=]*\[.*?\][=]*\]/s', '', $content); - $content = preg_replace('/[ \t]*#.*$/m', '', $content); - - return $content; -} - -/** - * Get all CMake files. - */ -function getAllCMakeFiles(string $path): Iterator -{ - if (is_file($path)) { - $info = new SplFileInfo($path); - - return new ArrayIterator([$info->getPathname() => $info]); - } - - $directoryIterator = new RecursiveDirectoryIterator($path, RecursiveDirectoryIterator::SKIP_DOTS); - $recursiveIterator = new RecursiveIteratorIterator(new RecursiveCallbackFilterIterator( - $directoryIterator, - function ($current, $key, $iterator) { - // Allow recursion. - if ($iterator->hasChildren()) { - return true; - } - - if ( - 1 === preg_match('/\.cmake$/', $current->getBasename()) - || in_array($current->getBasename(), [ - 'CMakeLists.txt', - 'CMakeLists.txt.in', - ], true) - ) { - return true; - } - - return false; - }, - )); - - $items = []; - foreach ($recursiveIterator as $item) { - $items[$item->getPathname()] = $item; - } - - return new ArrayIterator($items); -} - -/** - * Check given CMake files for include() issues. - */ -function checkCMakeInclude(Iterator $files, array $modules): int -{ - $status = 0; - - foreach ($files as $file) { - $content = getCMakeCode($file); - - // Check for redundant includes. - foreach ($modules as $module => $patterns) { - $hasModule = false; - $moduleEscaped = str_replace('/', '\/', $module); - - if (1 === preg_match('/^[ \t]*include[ \t]*\(' . $moduleEscaped . '[ \t]*\)/m', $content)) { - $hasModule = true; - } - - $hasPattern = false; - foreach ($patterns as $pattern) { - if (isRegularExpression($pattern)) { - if (1 === preg_match($pattern, $content)) { - $hasPattern = true; - break; - } - } elseif ( - ( - 1 === preg_match('/::/', $pattern) - && 1 === preg_match('/[^A-Za-z0-9_]' . $pattern . '[^A-Za-z0-9_]/m', $content) - ) - || 1 === preg_match('/^[ \t]*' . $pattern . '[ \t]*\(/m', $content) - ) { - $hasPattern = true; - break; - } - } - - // Skip current file if it is the current module itself. - $prefix = pathinfo(dirname($file->getRealPath()), PATHINFO_FILENAME); - $prefix = ('cmake' === $prefix) ? '' : $prefix . '/'; - $moduleNameFromFile = $prefix . pathinfo($file->getRealPath(), PATHINFO_FILENAME); - - if ($moduleNameFromFile == $module) { - continue; - } - - if ($hasModule && !$hasPattern) { - $status = 1; - output("E: redundant include($module) in $file"); - } - - if (!$hasModule && $hasPattern) { - $status = 1; - output("E: missing include($module) in $file"); - } - } - } - - return $status; -}; - -/** - * Check if given string is a regular expression. - */ -function isRegularExpression(string $string): bool -{ - set_error_handler(static function () {}, E_WARNING); - $isRegularExpression = false !== preg_match($string, ''); - restore_error_handler(); - - return $isRegularExpression; -} - -/** - * Check for set() usages with only one argument. These should be - * replaced with set( ""). - */ -function checkCMakeSet(Iterator $files): int -{ - $status = 0; - - foreach ($files as $file) { - $content = getCMakeCode($file); - - preg_match_all( - '/^[ \t]*set[ \t]*\([ \t\n]*([^ \t\)]+)[ \t]*\)/m', - $content, - $matches, - PREG_SET_ORDER, - ); - - foreach ($matches as $match) { - $argument = (array_key_exists(1, $match)) ? trim($match[1]) : ''; - - $status = 1; - output("E: Replace set($argument) with set($argument \"\")\n in $file\n"); - } - } - - return $status; -}; - -/** - * Check for obsolete usages of: - * else() - * endif() - * endforeach() - * endwhile() - * endfunction() - * endmacro() - */ -function checkCMakeObsoleteEndCommands(Iterator $files): int -{ - $status = 0; - - $commands = [ - 'else', - 'endif', - 'endforeach', - 'endwhile', - 'endfunction', - 'endmacro', - ]; - - foreach ($files as $file) { - $content = getCMakeCode($file); - - foreach ($commands as $command) { - preg_match_all( - '/^[ \t]*(' . $command . ')[ \t]*\((.+)\)/mi', - $content, - $matches, - PREG_SET_ORDER, - ); - - foreach ($matches as $match) { - $foundCommand = $match[1] ?? ''; - $foundArgument = $match[2] ?? ''; - - $status = 1; - output("E: Replace $foundCommand($foundArgument) with $command()\n in $file\n"); - } - } - } - - return $status; -} - -/** - * Find all local Find* modules in the project. - */ -function getFindModules(string $path): Iterator -{ - if (is_file($path)) { - $info = new SplFileInfo($path); - - return new ArrayIterator([$info->getPathname() => $info]); - } - - $directoryIterator = new RecursiveDirectoryIterator($path, RecursiveDirectoryIterator::SKIP_DOTS); - $recursiveIterator = new RecursiveIteratorIterator(new RecursiveCallbackFilterIterator( - $directoryIterator, - function ($current, $key, $iterator) { - // Allow recursion. - if ($iterator->hasChildren()) { - return true; - } - - if (preg_match('/^Find.*\.cmake$/', $current->getFilename())) { - return true; - } - - return false; - }, - )); - - $items = []; - foreach ($recursiveIterator as $item) { - $items[$item->getPathname()] = $item; - } - - return new ArrayIterator($items); -} - -/** - * Check for unused Find*.cmake modules. - */ -function checkFindModules(Iterator $findModules, Iterator $allCMakeFiles): int -{ - $status = 0; - - foreach ($findModules as $module) { - preg_match('/^Find(.*)\.cmake$/', $module->getFilename(), $matches); - $package = (array_key_exists(1, $matches)) ? $matches[1] : ''; - - $found = false; - foreach ($allCMakeFiles as $file) { - $content = getCMakeCode($file); - if (1 === preg_match('/find_package[ \t]*\([ \t\n\r]*' . $package . '[ \t\n\r\)]/', $content)) { - $found = true; - break; - } - } - - if (!$found) { - $status = 1; - output("E: unused find module $module"); - } - } - - return $status; -}; - -/** - * Find all local CMake modules in the project. - */ -function getModules(string $path): Iterator -{ - if (is_file($path)) { - $info = new SplFileInfo($path); - - return new ArrayIterator([$info->getPathname() => $info]); - } - - $directoryIterator = new RecursiveDirectoryIterator($path, RecursiveDirectoryIterator::SKIP_DOTS); - $recursiveIterator = new RecursiveIteratorIterator(new RecursiveCallbackFilterIterator( - $directoryIterator, - function ($current, $key, $iterator) { - // Allow recursion. - if ($iterator->hasChildren()) { - return true; - } - - if (preg_match('/^Find.*\.cmake$/', $current->getFilename())) { - return false; - } - - return true; - }, - )); - - $items = []; - foreach ($recursiveIterator as $item) { - $items[$item->getPathname()] = $item; - } - - return new ArrayIterator($items); -} - -/** - * Check for unused CMake modules. - */ -function checkModules(Iterator $modules, Iterator $allCMakeFiles): int -{ - $status = 0; - - foreach ($modules as $module) { - $namespace = ''; - $parent = dirname($module->getRealPath()); - $prefix = pathinfo($parent, PATHINFO_FILENAME); - $counter = 0; - while ($counter < 10 && 0 === preg_match('/cmake|modules/', $prefix)) { - $namespace = $prefix . '/' . $namespace; - $parent = dirname($parent); - $prefix = pathinfo($parent, PATHINFO_FILENAME); - ++$counter; - } - - $moduleNameEscaped = str_replace('/', '\/', $namespace . $module->getBasename('.cmake')); - $moduleBasenameEscaped = str_replace('.', '\.', $module->getBasename()); - - $found = false; - foreach ($allCMakeFiles as $file) { - $content = getCMakeCode($file); - - // Check if module is included with include(). - if (1 === preg_match('/include[ \t]*\([ \t\n\r]*' . $moduleNameEscaped . '[ \t\n\r\)]/', $content)) { - $found = true; - break; - } - - // Check if module is some artifact and used by its filename. - if (1 === preg_match('/' . $moduleBasenameEscaped . '/', $content)) { - $found = true; - break; - } - } - - if (!$found) { - $status = 1; - output("E: unused utility module $module"); - } - } - - return $status; -}; - -/** - * Check if terminal command exists. - */ -function checkCommand(string $command): bool -{ - $which = (\PHP_OS == 'WINNT') ? 'where' : 'which'; - - $process = \proc_open( - "$which $command", - [ - 0 => ['pipe', 'r'], // STDIN - 1 => ['pipe', 'w'], // STDOUT - 2 => ['pipe', 'w'], // STDERR - ], - $pipes, - ); - - if (false !== $process) { - $stdout = stream_get_contents($pipes[1]); - $stderr = stream_get_contents($pipes[2]); - fclose($pipes[1]); - fclose($pipes[2]); - proc_close($process); - - return '' != $stdout; - } - - return false; -} - -/** - * Check and run codespell tool. - */ -function runCodespell(): int -{ - if (!checkCommand('codespell')) { - output(<<] -P bin/update-cmake.cmake -# -# DIR - path to the directory containing CMake files. -# -# This is CMake-based command-line script that updates cmake_minimum_required() -# calls in all found CMake files with the cmake_minimum_required() as specified -# in this file (see the first line of the code below). - -cmake_minimum_required(VERSION 3.25...3.31) - -if(NOT CMAKE_SCRIPT_MODE_FILE) - message(FATAL_ERROR "This is a command-line script.") -endif() - -if(NOT DIR) - cmake_path(SET DIR NORMALIZE ${CMAKE_CURRENT_LIST_DIR}/../cmake) -else() - cmake_path( - ABSOLUTE_PATH - DIR - BASE_DIRECTORY ${CMAKE_CURRENT_LIST_DIR}/.. - NORMALIZE - ) -endif() - -if(NOT IS_DIRECTORY ${DIR}) - message(FATAL_ERROR "Directory not found: ${DIR}") -endif() - -string( - CONCAT regex - "cmake_minimum_required" - "[ \t]*" - "\\(" - "[ \t\n]*" - "VERSION" - "[ \t\n]+" - "[0-9]\\.[0-9]+[0-9.]*" - "(\\.\\.\\.[0-9]\\.[0-9]+)?" - "[ \t\n]*" - "(FATAL_ERROR)?" - "[ \t\n]*" - "\\)" -) - -# Get CMake minimum required version specification from this script. -file(STRINGS ${CMAKE_CURRENT_LIST_FILE} update REGEX "^${regex}" LIMIT_COUNT 1) -message(STATUS "Using: ${update}") - -message(STATUS "Checking CMake files in ${DIR}") - -file( - GLOB_RECURSE files - RELATIVE ${CMAKE_CURRENT_SOURCE_DIR} - ${DIR}/CMakeLists.txt - ${DIR}/CMakeLists.txt.in - ${DIR}/*.cmake - ${DIR}/*.cmake.in -) - -foreach(file IN LISTS files) - file(READ ${file} content) - - if(NOT content MATCHES "${regex}") - continue() - endif() - - string(REGEX REPLACE "${regex}" "${update}" newContent "${content}") - - if(newContent STREQUAL "${content}") - continue() - endif() - - message(STATUS "Updating ${file}") - file(WRITE ${file} "${newContent}") -endforeach()