Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 85 additions & 2 deletions beman_tidy/lib/checks/beman_standard/cmake.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from collections.abc import Iterable

import cmake_parser
from cmake_parser.ast import AstNode, Command
from cmake_parser.ast import AstNode, Command, If, Option

from beman_tidy.lib.utils.cmake import (
cmake_build_skip_subdir_option_pattern,
Expand Down Expand Up @@ -127,7 +127,90 @@ def get_cmake_target_names(self):
return cmake_target_names


# TODO cmake.default
@register_beman_standard_check("cmake.default")
class CMakeDefaultCheck(CMakeBaseCheck):
def __init__(self, repo_info, beman_standard_check_config):
super().__init__(repo_info, beman_standard_check_config)

def _is_library_add_library_command(self, command):
if command.identifier != "add_library":
return False
if not command.args:
return False
if command.args[0].value != self.library_name:
return False
if len(command.args) >= 2 and command.args[1].value == "ALIAS":
return False
return True

def _has_unconditional_library_target(self, ast_tree):
for item in ast_tree:
if isinstance(item, Command) and self._is_library_add_library_command(item):
return True
return False

def _get_option_defaults(self, ast_tree):
options = {}
for item in ast_tree:
if isinstance(item, Option) and item.args:
args = [arg.value for arg in item.args]
if args:
options[args[0]] = args[2] if len(args) >= 3 else None
return options

def _library_if_conditions(self, ast_tree):
conditions = []
for item in ast_tree:
if isinstance(item, If) and item.args:
condition = item.args[0].value
for sub in item.if_true or []:
if isinstance(sub, Command) and self._is_library_add_library_command(sub):
conditions.append(condition)
return conditions

def check(self):
ast_raw = self.get_cmake_parse_raw()
ast_tree = list(self.get_cmake_parse_tree())

has_library_in_raw = any(
self._is_library_add_library_command(item)
for item in ast_raw
if isinstance(item, Command)
)

if not has_library_in_raw:
self.log("CMake library target is not built by default. "
f"Expected unconditional add_library('{self.library_name}') in the root CMakeLists.txt. "
"Please update the CMakeLists.txt file according to the Beman Standard. "
"See https://github.com/bemanproject/beman/blob/main/docs/beman_standard.md#cmakedefault for more information.")
return False

if self._has_unconditional_library_target(ast_tree):
return True

option_defaults = self._get_option_defaults(ast_tree)
if_conditions = self._library_if_conditions(ast_tree)

for condition in if_conditions:
if condition in option_defaults and option_defaults[condition] == "OFF":
self.log("CMake library target is guarded by an option defaulting to OFF. "
f"The library target '{self.library_name}' must be built unconditionally. "
"Please update the CMakeLists.txt file according to the Beman Standard. "
"See https://github.com/bemanproject/beman/blob/main/docs/beman_standard.md#cmakedefault for more information.")
return False

self.log("CMake library target is only built conditionally. "
f"Expected unconditional add_library('{self.library_name}') in the root CMakeLists.txt. "
"Please update the CMakeLists.txt file according to the Beman Standard. "
"See https://github.com/bemanproject/beman/blob/main/docs/beman_standard.md#cmakedefault for more information.")
return False

def fix(self):
self.log(
"Please update the root CMakeLists.txt so that the main library target is built unconditionally by default. "
"See https://github.com/bemanproject/beman/blob/main/docs/beman_standard.md#cmakedefault for more information."
)
return False


# TODO cmake.use_find_package
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception

cmake_minimum_required(VERSION 3.30...4.3)

project(
beman.exemplar
DESCRIPTION "A Beman Library Exemplar"
LANGUAGES CXX
VERSION 0.1.0
)

option(
BEMAN_EXEMPLAR_BUILD_LIBRARY
"Enable building the library. Default: OFF. Values: { ON, OFF }."
OFF
)

if(BEMAN_EXEMPLAR_BUILD_LIBRARY)
add_library(beman.exemplar INTERFACE)
add_library(beman::exemplar ALIAS beman.exemplar)
endif()
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception

cmake_minimum_required(VERSION 3.30...4.3)

project(
beman.exemplar
DESCRIPTION "A Beman Library Exemplar"
LANGUAGES CXX
VERSION 0.1.0
)

if(PROJECT_IS_TOP_LEVEL)
add_library(beman.exemplar INTERFACE)
add_library(beman::exemplar ALIAS beman.exemplar)
endif()
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception

cmake_minimum_required(VERSION 3.30...4.3)

project(
beman.exemplar # CMake Project Name, which is also the name of the top-level
# targets (e.g., library, executable, etc.).
DESCRIPTION "A Beman Library Exemplar"
LANGUAGES CXX
VERSION 0.1.0
)

# [CMAKE.SKIP_TESTS]
option(
BEMAN_EXEMPLAR_BUILD_TESTS
"Enable building tests and test infrastructure. Default: ${PROJECT_IS_TOP_LEVEL}. Values: { ON, OFF }."
${PROJECT_IS_TOP_LEVEL}
)

# [CMAKE.SKIP_EXAMPLES]
option(
BEMAN_EXEMPLAR_BUILD_EXAMPLES
"Enable building examples. Default: ${PROJECT_IS_TOP_LEVEL}. Values: { ON, OFF }."
${PROJECT_IS_TOP_LEVEL}
)

# for find of beman_install_library and configure_build_telemetry
include(infra/cmake/beman-install-library.cmake)
include(infra/cmake/BuildTelemetryConfig.cmake)

add_library(beman.exemplar INTERFACE)
add_library(beman::exemplar ALIAS beman.exemplar)

target_sources(
beman.exemplar
PUBLIC FILE_SET HEADERS BASE_DIRS "${CMAKE_CURRENT_SOURCE_DIR}/include"
)

set_target_properties(
beman.exemplar
PROPERTIES VERIFY_INTERFACE_HEADER_SETS ${PROJECT_IS_TOP_LEVEL}
)

add_subdirectory(include/beman/exemplar)

beman_install_library(beman.exemplar TARGETS beman.exemplar)
configure_build_telemetry()

if(BEMAN_EXEMPLAR_BUILD_TESTS)
enable_testing()
add_subdirectory(tests/beman/exemplar)
endif()

if(BEMAN_EXEMPLAR_BUILD_EXAMPLES)
add_subdirectory(examples)
endif()
49 changes: 49 additions & 0 deletions tests/lib/checks/beman_standard/cmake/test_cmake.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

# Actual tested checks.
from beman_tidy.lib.checks.beman_standard.cmake import (
CMakeDefaultCheck,
CMakeProjectNameCheck,
CMakeLibraryNameCheck,
CMakeLibraryAliasCheck,
Expand All @@ -23,6 +24,54 @@
invalid_prefix = f"{test_data_prefix}/invalid"


def test__cmake_default__valid(repo_info, beman_standard_check_config):
"""
Test that a valid CMakeLists.txt file passes the cmake.default check.
"""
valid_cmake_paths = [
# CMakeLists.txt from beman.exemplar
Path(f"{valid_prefix}/valid-default-v1.txt"),
]

run_check_for_each_path(
True,
valid_cmake_paths,
CMakeDefaultCheck,
repo_info,
beman_standard_check_config,
)


def test__cmake_default__invalid(repo_info, beman_standard_check_config):
"""
Test that an invalid CMakeLists.txt file fails the cmake.default check.
"""
invalid_cmake_paths = [
# CMakeLists.txt with library behind OFF option
Path(f"{invalid_prefix}/invalid-default-v1.txt"),
# CMakeLists.txt with library only inside if()
Path(f"{invalid_prefix}/invalid-default-v2.txt"),
]

run_check_for_each_path(
False,
invalid_cmake_paths,
CMakeDefaultCheck,
repo_info,
beman_standard_check_config,
)


@pytest.mark.skip(reason="fix-inplace not yet implemented: cmake.default is check-only")
def test__cmake_default__fix_inplace(repo_info, beman_standard_check_config):
"""
Test that the fix method corrects an invalid CMakeLists.txt file.
Note: Skipping this test as fix-inplace is not implemented for cmake.default.
The check is currently check-only; fix() logs guidance and returns False.
"""
pass


def test__cmake_project_name__valid(repo_info, beman_standard_check_config):
"""
Test that a valid CMakeLists.txt file passes the cmake.project_name check.
Expand Down