# State Management Standards for CMake
This document outlines the coding standards for managing state within CMake projects. Effective state management is crucial for maintainability, reconfigurability, and build system integrity. It specifically addresses how to handle data flow, reactivity, and persistent or transient variables within the CMake environment.
## 1. Introduction to State Management in CMake
State management in CMake pertains to how your build configuration stores, accesses, and modifies information throughout the build process. This includes managing variables, properties of targets and directories, and cached values. Poor state management can lead to unpredictable builds, difficult debugging, and code duplication. Efficient state management practices are important since CMake code runs during the configuration step, making it critical to ensure this step is fast and correct.
## 2. Core Principles
* **Explicit Declaration:** Always declare variables before using them. This reduces ambiguity and improves readability. Use "set()" for regular variables and "option()" for user-configurable options.
* **Scope Awareness:** Be mindful of variable scope (function, directory, global). Avoid unintentionally modifying variables in unrelated scopes. Use "set(VAR VALUE PARENT_SCOPE)" carefully.
* **Immutability (Where Possible):** Treat variables as immutable whenever feasible, particularly those that represent build constants. Avoid in-place modification when possible.
* **Clear Data Flow:** Establish a clear flow of data between different CMake modules. Avoid global variables or implicit dependencies that can obscure the state's origin.
* **Reactivity:** Use CMake's features for responding to changes in the environment or user-defined options. This includes using "if()" conditions, "target_compile_definitions()" with preprocessor macros, and appropriate "find_package()" mechanisms.
* **Idempotence:** A key characteristic of CMake scripts is idempotence. Running CMake multiple times without changes to the input should result in the same output. Your state management must respect this principle.
## 3. Variable Management
### 3.1. Variable Scope
CMake has three primary scopes for variables:
* **Function Scope:** Variables defined inside a function using "set()" are local to that function.
* **Directory Scope:** Variables defined in a "CMakeLists.txt" file using "set()" are local to that directory and all subdirectories, unless explicitly overridden.
* **Global Scope (Cache):** Variables stored in the cache (e.g., using "option()" or "set(VAR VALUE CACHE ...)"). These persist across CMake runs.
**Do This:**
* Use function scope variables whenever possible to encapsulate logic and avoid unintended side effects.
"""cmake
function(my_function)
set(local_variable "some value") # Function scope
message("Local variable: ${local_variable}")
endfunction()
my_function()
message("Local variable (outside function): ${local_variable}") # Empty
"""
* Use directory scope variables for configurations relevant to all targets in a given directory.
"""cmake
# CMakeLists.txt (at the project root)
set(PROJECT_SOURCE_DIR ${CMAKE_SOURCE_DIR}) # Directory Scope
add_subdirectory(src)
# src/CMakeLists.txt can access PROJECT_SOURCE_DIR
"""
* Use cache variables ("option()", "set(VAR VALUE CACHE ...)") for settings the user can configure.
"""cmake
option(ENABLE_FEATURE "Enable the cool feature" ON)
if(ENABLE_FEATURE)
message("Feature is enabled.")
endif()
"""
**Don't Do This:**
* Overuse global (cache) variables. This creates hidden dependencies and makes it difficult to reason about the state of the build system.
* Modify variables in parent scopes unless absolutely necessary, and always document the intention clearly.
### 3.2. Variable Naming Conventions
* **Local Variables:** "lowercase_with_underscores" (e.g., "loop_counter", "file_name").
* **Directory-Scoped Variables:** "UPPER_SNAKE_CASE" (e.g., "CMAKE_CXX_FLAGS", "PROJECT_VERSION").
* **Cache Variables:** "UPPER_SNAKE_CASE" (e.g., "ENABLE_FEATURE", "INSTALL_PREFIX"), often prefixed with the project name, like "MYPROJECT_ENABLE_FEATURE".
**Do This:**
* Follow the recommended naming conventions consistently to improve readability and intent.
* Use meaningful names that clearly describe the variable's purpose.
**Don't Do This:**
* Use cryptic or abbreviated variable names (unless in very localized contexts).
### 3.3. Setting and Getting Variables
* Use "set(VAR VALUE)" to set a variable. To append to a CMake list, prefer "list(APPEND VAR VALUE)" for correctness. Make sure to enclose variables in double quotes to prevent evaluation issues.
**Do This:**
"""cmake
set(MY_VARIABLE "Hello, world!")
message("My variable: ${MY_VARIABLE}")
list(APPEND MY_LIST "item1")
list(APPEND MY_LIST "item2")
message("My list: ${MY_LIST}")
"""
**Don't Do This:**
* "set(VAR VALUE)" without quotes can lead to unexpected behavior if "VALUE" contains spaces or other variables.
### 3.4. Working with Lists
* Use "list()" operations ("APPEND", "INSERT", "REMOVE_ITEM", "LENGTH", "GET", etc.) to manipulate lists efficiently.
**Do This:**
"""cmake
set(MY_LIST "item1;item2;item3") # String representation of list, less safe
list(APPEND MY_LIST "item4") # Recommended
list(LENGTH MY_LIST list_length)
message("List length: ${list_length}")
foreach(item IN LISTS MY_LIST)
message("Item: ${item}")
endforeach()
"""
**Don't Do This:**
* Avoid string manipulation to work with lists (e.g., using "string(REPLACE)"). The "list" commands are specifically designed for this purpose. Modifying the CMake list variable using "set" directly might cause issues if list elements contains semicolons, which are used as the list separator.
### 3.5 Handling Cached Variables
* Cached variables, set with the "CACHE" option in the "set" command, persist between CMake runs and are typically used for user-configurable settings. The "option()" command is a convenient wrapper around the "set(... CACHE ...)" command for boolean options.
**Do This:**
"""cmake
option(ENABLE_DEBUG "Enable debug build" OFF) # boolean option
if(ENABLE_DEBUG)
add_definitions(-DDEBUG)
endif()
set(INSTALL_PREFIX "/opt/myproject" CACHE PATH "Installation directory")
"""
**Don't Do This:**
* Avoid storing temporary or intermediate values in the cache. This pollutes the cache and can lead to incorrect builds.
* Don't expose internal implementation details as cache variables; only expose options that a user or external system needs to configure.
## 4. Properties Management
CMake properties are name-value pairs associated with targets, directories, source files, and other entities. They provide a mechanism to store and retrieve metadata. Properties are often used to manage compiler flags, link libraries, include directories, and other build-related settings.
### 4.1. Target Properties
* Use "set_property(TARGET ... PROPERTY ...)" and "get_property(TARGET ... PROPERTY ...)" to manage target-specific settings. Common properties include:
* "COMPILE_DEFINITIONS": Preprocessor definitions.
* "COMPILE_FLAGS": Compiler flags.
* "INCLUDE_DIRECTORIES": Include directories.
* "LINK_LIBRARIES": Libraries to link against.
* "POSITION_INDEPENDENT_CODE": Whether to generate position-independent code.
**Do This:**
"""cmake
add_library(mylibrary SHARED mylibrary.cpp)
set_property(TARGET mylibrary PROPERTY CXX_STANDARD 17)
set_property(TARGET mylibrary PROPERTY CXX_STANDARD_REQUIRED ON)
set_property(TARGET mylibrary PROPERTY POSITION_INDEPENDENT_CODE ON)
target_compile_definitions(mylibrary PRIVATE DEBUG_MODE) # Modern alternative
get_property(cxx_standard TARGET mylibrary PROPERTY CXX_STANDARD)
message("C++ standard: ${cxx_standard}")
"""
* Use "target_*" convenience commands ("target_compile_definitions()", "target_include_directories()", "target_link_libraries()") to manage specific target properties where available these provide superior dependency management and visibility. These commands are preferred to direct setting of properties like "COMPILE_DEFINITIONS".
**Don't Do This:**
* Modify target properties directly using "set()" or "set_property()" unless necessary. The "target_*" commands provide better dependency management and are generally safer and more readable. Modifying properties directly can lead to shadowing issues where the property is not correctly inherited.
### 4.2. Directory Properties
* Use "set_property(DIRECTORY ... PROPERTY ...)" and "get_property(DIRECTORY ... PROPERTY ...)" to manage directory-specific settings. This will set the properties for the current directory and all subdirectories.
**Do This:**
"""cmake
set_property(DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} PROPERTY INCLUDE_DIRECTORIES "${CMAKE_CURRENT_SOURCE_DIR}/include")
get_property(include_dirs DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} PROPERTY INCLUDE_DIRECTORIES)
message("Include dirs in this directory: ${include_dirs}")
"""
**Don't Do This:**
* Overuse directory properties. This can make it difficult to track down where settings are coming from. Prefer target-specific properties when possible.
### 4.3. Source File Properties
* Use "set_property(SOURCE ... PROPERTY ...)" and "get_property(SOURCE ... PROPERTY ...)" to manage source file-specific settings (e.g., compiler flags that apply only to one file).
**Do This:**
"""cmake
set_property(SOURCE my_special_file.cpp PROPERTY COMPILE_FLAGS "-Wno-unused-variable")
"""
**Don't Do This:**
* Apply the same property to a large number of source files this is a sign of a configuration management problem.
### 4.4. Using Interface Properties
* Interface Properties are extremely useful for managing dependencies and propagating information between targets, especially in library projects.
**Do This:**
"""cmake
add_library(MyInterfaceLib INTERFACE)
set_property(TARGET MyInterfaceLib PROPERTY INTERFACE_INCLUDE_DIRECTORIES "${CMAKE_CURRENT_SOURCE_DIR}/include")
add_library(MyLib ...)
target_link_libraries(MyLib INTERFACE MyInterfaceLib) # Propagates interface properties to targets linking MyLib
"""
**Don't Do This:**
* Forget to actually use the INTERFACE target by linking to it. Declaring the properties on an INTERFACE target without linking to it defeats the purpose.
## 5. Configuration Options
* Use "option()" to define user-configurable options. This creates a cache variable with a default value that the user can override.
**Do This:**
"""cmake
option(USE_MY_LIBRARY "Use my custom library" OFF)
if(USE_MY_LIBRARY)
find_package(MyLibrary REQUIRED)
if(MyLibrary_FOUND)
target_link_libraries(mytarget MyLibrary::MyLibrary)
endif()
endif()
"""
**Don't Do This:**
* Avoid hardcoding configuration options. Use "option()" to allow users to customize the build.
* Don't create options without default values as this may lead to undefined behavior.
## 6. Handling External Dependencies
* Use "find_package()" to locate external libraries and their associated information (include directories, libraries to link against, etc.).
**Do This:**
"""cmake
find_package(Boost REQUIRED COMPONENTS system filesystem)
if(Boost_FOUND)
include_directories(${Boost_INCLUDE_DIRS})
target_link_libraries(mytarget Boost::system Boost::filesystem)
endif()
"""
* Prefer modern "find_package()" usage with Config mode (e.g., "find_package(MyLibrary REQUIRED) target_link_libraries(mytarget MyLibrary::MyLibrary)") over legacy Module mode, which relies on global variables.
**Don't Do This:**
* Hardcode paths to external libraries. Use "find_package()" to locate them dynamically.
* Manually set include directories and link libraries for external dependencies, always use "find_package()" results if dependencies provide them.
## 7. Implementing reactivity with "if()" Statements
* CMake's "if()" statement allows you to conditionally execute code blocks based on variable values, properties, or other conditions.
* Utilize "if()" with "DEFINED", "NOT DEFINED" to check for the existence of variables.
* For string comparisons, use "STREQUAL", "EQUAL", "MATCHES" or "VERSION_GREATER" for version comparisons
* Combine conditions using "AND", "OR" and "NOT".
**Do This:**
"""cmake
if(CMAKE_BUILD_TYPE STREQUAL "Debug")
message("Debug build")
add_definitions(-DDEBUG)
elseif(CMAKE_BUILD_TYPE STREQUAL "Release")
message("Release build")
add_definitions(-DNDEBUG)
endif()
if(NOT DEFINED MY_CUSTOM_VARIABLE)
set(MY_CUSTOM_VARIABLE "default value")
endif()
"""
**Don't Do This:**
* Avoid complex nested "if()" statements. Decompose the logic into smaller functions or macros for better readability.
* Don't use "if(variable)" to test if a variable has any content. "if(variable)" in CMake returns true if the variable is defined AND is not a false boolean constant (OFF, FALSE, NO, N, 0, or an empty string). Use "if(DEFINED variable)" and "if(NOT "${variable}" STREQUAL "")" to determine if a variable has a non-empty value.
## 8. Macros and Functions for State Management
* Use macros and functions to encapsulate complex state management logic and promote code reuse. Functions are generally preferred over macros because they have their own scopes.
**Do This:**
"""cmake
function(configure_my_library target_name)
target_include_directories(${target_name} PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/include")
target_compile_definitions(${target_name} PUBLIC USE_MY_LIBRARY)
endfunction()
add_library(mylibrary ...)
configure_my_library(mylibrary)
"""
**Don't Do This:**
* Create overly complex macros or functions that are difficult to understand or modify.
* Forget local scope of the function while relying on global state. Try to pass all required variables as arguments.
## 9. Modern CMake State Management patterns
### 9.1 Using Generator Expressions
Generator expressions in CMake allow you to defer evaluation of certain expressions until the build system is generated. This is extremely useful for conditional logic that depends on the build configuration or target properties.
**Do This:**
"""cmake
add_library(mylibrary ...)
target_compile_definitions(mylibrary PRIVATE $<$:DEBUG_MODE>) # Only define DEBUG_MODE in Debug builds
target_link_libraries(myexe PRIVATE $<$:ws2_32>) # Only add ws2_32 on Windows
"""
**Don't Do This:**
* Avoid needlessly complex generator expressions that hinder readability.
* Make sure you understand when generator expressions are evaluated (during the build system generation phase) and not at CMake configuration time.
### 9.2 Using "export()" and "install()" for Package Management
Modern CMake heavily relies on exporting targets and installing the corresponding configuration files. This makes it much easier for other projects to use your library.
**Do This:**
"""cmake
install(TARGETS mylibrary
EXPORT mylibrary-export
DESTINATION lib)
install(FILES mylibrary.h
DESTINATION include)
export(TARGETS mylibrary
FILE mylibrary-export.cmake) # Generates a CMake config file allowing external projects to easily use MyLibrary
install(EXPORT mylibrary-export DESTINATION share/MyLibrary/cmake) # Installs mylibrary-export for installation
#In the downstream project use:
#find_package(MyLibrary REQUIRED)
#target_link_libraries(DownstreamTarget MyLibrary::MyLibrary)
"""
**Don't Do This:**
* Forget to export your targets. This makes it difficult for other projects to use your library.
* Mix install() of targets and files destined for different locations to prevent deployment and runtime issues.
### 9.3 Using Modules
* Use CMake modules (".cmake" files) to organize and share CMake code. This promotes code reuse and improves the overall structure of your project.
**Do This:**
"""cmake
# mymodule.cmake
function(my_custom_function)
message("This is a custom function.")
endfunction()
# CMakeLists.txt
include(mymodule.cmake)
my_custom_function()
"""
**Don't Do This:**
* Create overly large modules that are difficult to navigate. Keep modules focused on specific tasks or functionalities.
## 10. Security Considerations
* Be careful about using user-provided input in CMake scripts, especially when constructing commands or paths. Sanitize inputs to prevent command injection vulnerabilities.
**Do This:**
"""cmake
# Sanitize user-provided input
string(REPLACE ";" "\\;" sanitized_path "${USER_INPUT_PATH}")
execute_process(COMMAND ${CMAKE_COMMAND} -E make_directory "${sanitized_path}")
"""
**Don't Do This:**
* Directly use user-provided input in "execute_process()" without sanitization.
## 11. Performance Considerations
* Reduce the CMake configuration time by:
* Avoid unnecessary computations or lookups during configuration e.g. avoid recursive macro expansion.
* Strive to minimize directory iterations, especially in large projects.
* Use precompiled headers for your library to speed up compilation.
* Prefer "target_*" commands over manual manipulation of properties for optimization.
* Use "cmake --install" for efficient installation after the build completes.
## 12. Conclusion
By adhering to these coding standards, you can create CMake projects that are maintainable, reliable, secure, and efficient in state management. Consistent application of these practices improves code readability, reduces the risk of errors, and simplifies collaboration among developers. Regularly review and update these standards to align with the latest best practices and technological advancements in CMake development.
danielsogl
Created Mar 6, 2025
This guide explains how to effectively use .clinerules
with Cline, the AI-powered coding assistant.
The .clinerules
file is a powerful configuration file that helps Cline understand your project's requirements, coding standards, and constraints. When placed in your project's root directory, it automatically guides Cline's behavior and ensures consistency across your codebase.
Place the .clinerules
file in your project's root directory. Cline automatically detects and follows these rules for all files within the project.
# Project Overview project: name: 'Your Project Name' description: 'Brief project description' stack: - technology: 'Framework/Language' version: 'X.Y.Z' - technology: 'Database' version: 'X.Y.Z'
# Code Standards standards: style: - 'Use consistent indentation (2 spaces)' - 'Follow language-specific naming conventions' documentation: - 'Include JSDoc comments for all functions' - 'Maintain up-to-date README files' testing: - 'Write unit tests for all new features' - 'Maintain minimum 80% code coverage'
# Security Guidelines security: authentication: - 'Implement proper token validation' - 'Use environment variables for secrets' dataProtection: - 'Sanitize all user inputs' - 'Implement proper error handling'
Be Specific
Maintain Organization
Regular Updates
# Common Patterns Example patterns: components: - pattern: 'Use functional components by default' - pattern: 'Implement error boundaries for component trees' stateManagement: - pattern: 'Use React Query for server state' - pattern: 'Implement proper loading states'
Commit the Rules
.clinerules
in version controlTeam Collaboration
Rules Not Being Applied
Conflicting Rules
Performance Considerations
# Basic .clinerules Example project: name: 'Web Application' type: 'Next.js Frontend' standards: - 'Use TypeScript for all new code' - 'Follow React best practices' - 'Implement proper error handling' testing: unit: - 'Jest for unit tests' - 'React Testing Library for components' e2e: - 'Cypress for end-to-end testing' documentation: required: - 'README.md in each major directory' - 'JSDoc comments for public APIs' - 'Changelog updates for all changes'
# Advanced .clinerules Example project: name: 'Enterprise Application' compliance: - 'GDPR requirements' - 'WCAG 2.1 AA accessibility' architecture: patterns: - 'Clean Architecture principles' - 'Domain-Driven Design concepts' security: requirements: - 'OAuth 2.0 authentication' - 'Rate limiting on all APIs' - 'Input validation with Zod'
# Testing Methodologies Standards for CMake This document outlines the coding standards for testing methodologies in CMake projects. It covers unit, integration, and end-to-end testing strategies specifically within the CMake context, emphasizing modern approaches and best practices. ## 1. General Testing Principles ### 1.1 Clarity and Organization * **Do This:** Structure tests clearly, with descriptive names for test files, test targets, and individual test cases. Aim for self-documenting tests that explain WHAT is being tested and WHY. * **Don't Do This:** Use cryptic or abbreviated names that obscure the purpose of the test. Avoid large, monolithic test files that test too many unrelated components. * **Why:** Clarity enhances maintainability. Well-organized tests are easier to understand, debug, and extend, preventing test rot and promoting confidence in the codebase. """cmake # Good: add_test(NAME MyLibrary_StringUtil_Trim_LeadingWhitespace COMMAND ${CMAKE_CTEST_COMMAND} -C $<CONFIG> -R "MyLibrary_StringUtil_Trim_LeadingWhitespace" ) # Bad: add_test(NAME test1 COMMAND ${CMAKE_CTEST_COMMAND} -C $<CONFIG> -R "test1") """ ### 1.2 Test-Driven Development (TDD) * **Do This:** Consider adopting TDD principles, writing tests *before* implementing the code that satisfies them. * **Don't Do This:** Write tests as an afterthought. Tests should drive the design and implementation of the feature. * **Why:** TDD encourages better design, reduces bugs, and leads to more testable code. It also provides immediate feedback on changes. ### 1.3 Independent Tests * **Do This:** Ensure each test is independent from other tests. Tests should not rely on the side effects of previous tests. * **Don't Do This:** Create test suites with dependencies between tests. This makes debugging and reasoning about failures extremely difficult and can lead to cascading issues. * **Why:** Independence prevents cascading failures and makes tests easier to reason about and debug. ### 1.4 Test Data Management * **Do This:** Use realistic and relevant test data, but avoid complex or excessive data that obscures the test's purpose. Consider generating test data programmatically for complex scenarios. * **Don't Do This:** Hardcode unrealistic or minimal test data that doesn't reflect real-world use cases. * **Why:** Realistic test data increases confidence that the code works in production-like environments. ## 2. Unit Testing ### 2.1 Framework Selection * **Do This:** Use a modern C++ testing framework like Google Test, Catch2, or doctest. Integrate the framework into your CMake project using "FetchContent" or "find_package". * **Don't Do This:** Roll your own testing framework. Rely on "printf"-style debugging or manual inspection. * **Why:** Established frameworks provide powerful features like assertions, test organization, mocking, and reporting. """cmake # Example using FetchContent and Google Test (gtest) include(FetchContent) FetchContent_Declare( googletest GIT_REPOSITORY https://github.com/google/googletest.git GIT_TAG v1.14.0 # Use a specific version ) FetchContent_MakeAvailable(googletest) add_executable(MyLibraryTests test/MyLibraryTests.cpp) target_link_libraries(MyLibraryTests MyLibrary gtest_main) include(GoogleTest) gtest_discover_tests(MyLibraryTests) """ ### 2.2 Test Structure * **Do This:** Organize unit tests into well-defined test cases targeting specific functions or classes. Use a common pattern for test case naming (e.g., "ClassName_MethodName_Scenario"). * **Don't Do This:** Mix multiple tests within a single large test function. Use overly broad test cases that try to test everything at once. * **Why:** Fine-grained tests pinpoint the source of failures more easily than blanket tests. """cpp // Example using Google Test #include "MyLibrary.h" #include "gtest/gtest.h" TEST(StringUtil_Trim_LeadingWhitespace, EmptyString) { std::string str = ""; MyLibrary::trim(str); ASSERT_EQ(str, ""); } TEST(StringUtil_Trim_LeadingWhitespace, StringWithLeadingSpaces) { std::string str = " Hello"; MyLibrary::trim(str); ASSERT_EQ(str, "Hello"); } """ ### 2.3 Mocking and Isolation * **Do This:** Use mocking frameworks (e.g., Google Mock) to isolate units under test from external dependencies (e.g., databases, network services, hardware). Prefer dependency injection over global state. * **Don't Do This:** Directly access external resources or rely on global state within unit tests. This makes tests slow, non-deterministic, and difficult to run in isolation. * **Why:** Mocking ensures that tests are fast, repeatable, and focus on the behavior of the unit under test. ### 2.4 CMake Integration * **Do This:** Create a separate executable target for unit tests. Use "add_test" to register the tests with CTest. Leverage "ctest_discover_tests" to automatically discover tests. * **Don't Do This:** Embed test code directly within library or application code. Manually invoke test executables. * **Why:** Separating tests allows for clean separation of concerns and enables automated test execution within the CMake build process. "ctest_discover_tests" significantly simplifies test management. """cmake # Modern CMake: Automating with "ctest_discover_tests" add_executable(MyLibraryTests test/MyLibraryTests.cpp) target_link_libraries(MyLibraryTests MyLibrary gtest_main) include(GoogleTest) gtest_discover_tests(MyLibraryTests) # Automatically find and run tests """ ## 3. Integration Testing ### 3.1 Scope * **Do This:** Focus integration tests on verifying the interactions between multiple components or modules. Aim to test the interfaces and data flows between units. * **Don't Do This:** Use integration tests to replicate unit tests (testing individual functions in isolation). * **Why:** Integration tests ensure that the parts of a system work together correctly, catching issues that unit tests might miss. ### 3.2 Environments * **Do This:** Define integration test environments with clear dependencies and configurations. Automate the setup and teardown of these environments. * **Don't Do This:** Run integration tests against production environments. Use inconsistent or manually configured environments. * **Why:** Predictable environments make integration tests reliable and repeatable. ### 3.3 Data Persistence * **Do This:** If testing code that interacts with databases or file systems, use dedicated testing databases or directories. Clear and reset the data after each test. * **Don't Do This:** Write to production databases or modify important files during testing. * **Why:** Avoid data corruption or unintended side effects on other systems. ### 3.4 CMake Integration * **Do This:** Package integration tests as separate executables. Use "add_test" to register these tests with CTest, similar to unit tests. Consider using CMake's "execute_process" command to control external processes or services during testing. * **Don't Do This:** Manually run integration tests or rely on external scripts outside of the CMake build process. * **Why:** Integrate into Continuous Integration (CI/CD) pipelines for automatic execution and reporting. """cmake # Example integration test using CMake's "execute_process" add_executable(IntegrationTests test/IntegrationTests.cpp) target_link_libraries(IntegrationTests MyLibrary) # Defines custom target to start a mock service (replace with actual service) add_custom_target(start_mock_service COMMAND ${CMAKE_COMMAND} -E echo "Starting mock service..." COMMAND ${CMAKE_COMMAND} -E sleep 1 # Simulate startup time WORKING_DIRECTORY ${CMAKE_BINARY_DIR} ) # Defines custom target to stop the mock service (replace with actual service) add_custom_target(stop_mock_service COMMAND ${CMAKE_COMMAND} -E echo "Stopping mock service..." COMMAND ${CMAKE_COMMAND} -E sleep 1 WORKING_DIRECTORY ${CMAKE_BINARY_DIR} ) # Integration test definition add_test( NAME IntegrationTest_MyFeature COMMAND ${CMAKE_COMMAND} -E env "MOCK_SERVICE_URL=http://localhost:8080" $<TARGET_FILE:IntegrationTests> WORKING_DIRECTORY ${CMAKE_BINARY_DIR} DEPENDS start_mock_service stop_mock_service #Run start and stop custom targets ) set_property(TEST IntegrationTest_MyFeature PROPERTY ENVIRONMENT "MOCK_ENABLED=1") #Example Environment var """ ## 4. End-to-End (E2E) Testing ### 4.1 Scope * **Do This:** Design E2E tests to simulate real user workflows, exercising the entire system from input to output. These tests validate that all system components work together to deliver the desired functionality. * **Don't Do This:** Focus on low-level details or internal component interactions. Treat the system as a black box. * **Why:** E2E tests provide the highest level of confidence that the system works as expected from a user's perspective. ### 4.2 Test Automation * **Do This:** Automate E2E tests using tools like Selenium, Cypress, or custom-built scripts. Integrate these tests into your CI/CD pipeline. * **Don't Do This:** Rely on manual E2E testing or sporadic, ad-hoc testing. Requires fully automated, repeatable execution. * **Why:** Automation ensures consistent and reliable E2E testing, enabling rapid feedback on changes. ### 4.3 Environment Configuration * **Do This:** Ensure the E2E test environment closely mimics the production environment, including infrastructure, configuration, and data. Consider using containerization technologies (e.g., Docker) to create consistent environments. * **Don't Do This:** Test against development or staging environments that differ significantly from production. * **Why:** Production-like environments maximize the likelihood of detecting issues before they reach users. ### 4.4 Test Data Management * **Do This:** Seed the E2E test environment with representative data to simulate real-world usage. Clean up and reset the data after each test run. * **Don't Do This:** Use minimal or unrealistic data that doesn't expose potential issues. * **Why:** Realistic data ensures the E2E tests accurately reflect the behavior of the system in production. ### 4.5 CMake Integration * **Do This:** Create CMake custom commands to manage E2E test setup, execution, and teardown. Use "add_test" to integrate these commands with CTest. Employ "execute_process" to run external test tools or scripts. * **Don't Do This:** Run E2E tests outside of the CMake build process. * **Why:** CMake integration allows for seamless E2E test execution within your build workflow. """cmake # Example E2E test using CMake and a hypothetical E2E test script add_custom_command( OUTPUT ${CMAKE_BINARY_DIR}/e2e_test_result.txt COMMAND ${CMAKE_COMMAND} -E echo "Running End-to-End Tests..." COMMAND python ${CMAKE_SOURCE_DIR}/test/e2e_tests.py --output ${CMAKE_BINARY_DIR}/e2e_test_result.txt DEPENDS MyApplication # Assuming MyApplication needs to be built first WORKING_DIRECTORY ${CMAKE_BINARY_DIR} # Set working directory correctly ) add_test(NAME EndToEndTest COMMAND ${CMAKE_COMMAND} -E true #Test always "passes", result is parsed from file. Update status appropriately. DEPENDS ${CMAKE_BINARY_DIR}/e2e_test_result.txt #Ensure Command is run. ) """ ## 5. Test Reporting and Analysis ### 5.1 CTest Integration * **Do This:** Use CTest's reporting capabilities to generate test summaries and dashboards. Configure CTest to upload test results to a CI/CD server (e.g., Jenkins, GitLab CI, GitHub Actions) or a dedicated test reporting tool (e.g., CDash). * **Don't Do This:** Ignore CTest's reporting features or rely on manual inspection of test output. * **Why:** CTest provides centralized test management and reporting, enabling you to track test results, identify trends, and quickly diagnose failures. ### 5.2 Code Coverage * **Do This:** Integrate code coverage tools (e.g., gcov, lcov) into your CMake build process. Use these tools to measure the percentage of code covered by your tests. Strive for high code coverage to minimize the risk of undetected bugs. * **Don't Do This:** Neglect code coverage analysis or assume that tests adequately cover all code paths. * **Why:** Code coverage analysis helps you identify gaps in your testing and improve the overall quality of your codebase. ### 5.3 Static Analysis * **Do This:** Integrate static analysis tools (e.g., Clang Static Analyzer, cppcheck) into your CMake build process. Use these tools to identify potential code defects, security vulnerabilities, and style violations. * **Don't Do This:** Ignore static analysis warnings or suppress them without careful consideration. * **Why:** Static analysis can detect issues early in the development cycle, before they become more costly to fix. ## 6. Avoiding Common Anti-Patterns ### 6.1 "Magic Strings" and Hardcoded Values * **Don't Do This:** Use "magic strings" or hardcoded values in test code. * **Do This:** Define constants or configuration parameters for test data, file paths, and other values. This is a common pattern. * **Why:** Improves readability and maintainability. ### 6.2 Over-Reliance on Mocks * **Don't Do This:** Mock everything. * **Do This:** Use mocks strategically to isolate units under test, but also include integration tests to verify real interactions. * **Why:** Balance unit and integration testing. ### 6.3 Ignoring Failing Tests * **Don't Do This:** Ignore failing tests or disable them without fixing the underlying issues. * **Do This:** Treat failing tests as high-priority bugs. * **Why:** Prevents test rot and ensures code quality. ### 6.4 Testing Implementation Details * **Don't Do This:** Write tests that are tightly coupled to the implementation details of the code they test. * **Do This:** Focus on testing the public interface and the expected behavior of the code. * **Why:** Reduces test fragility and makes it easier to refactor code without breaking tests.
# Security Best Practices Standards for CMake This document outlines security best practices for CMake projects, aiming to mitigate vulnerabilities and promote secure coding patterns. These guidelines are designed to be used by developers and AI coding assistants to ensure robust and secure CMake configurations. ## 1. Input Validation and Sanitization ### Standard 1.1: Validate All External Inputs **Do This:** Validate all external inputs (e.g., command-line arguments, environment variables, file contents) before using them in CMake commands. Use regular expressions, string manipulation functions, and CMake’s built-in checks to ensure inputs meet expected formats and constraints. **Don't Do This:** Directly use untrusted external inputs in CMake commands without validation, as this can lead to command injection, path traversal, or other vulnerabilities. **Why:** Input validation prevents malicious actors from manipulating your build process via crafted inputs. **Example:** """cmake # Validating a command-line option option(ENABLE_DEBUG "Enable debug mode" OFF) if(ENABLE_DEBUG) message(STATUS "Debug mode enabled.") add_definitions(-DDEBUG) endif() # Validating an environment variable if(DEFINED ENV{CUSTOM_PATH}) string(REGEX MATCH "^/opt/custom_path(/.*)?$" CUSTOM_PATH_VALID "${ENV{CUSTOM_PATH}}") if(CUSTOM_PATH_VALID) message(STATUS "Custom path is valid: ${ENV{CUSTOM_PATH}}") set(CUSTOM_PATH "${ENV{CUSTOM_PATH}}" CACHE PATH "Custom installation path") else() message(FATAL_ERROR "Invalid custom path: ${ENV{CUSTOM_PATH}}") endif() endif() """ ### Standard 1.2: Sanitize Path Inputs **Do This:** Sanitize path inputs using "file(TO_CMAKE_PATH)" and "file(REAL_PATH)" to prevent path traversal attacks. Prefer absolute paths and avoid relative paths when possible. **Don't Do This:** Directly concatenate user-provided path segments without sanitization, or use relative paths without proper context. **Why:** Sanitizing paths prevents an attacker from using "../" sequences to access files or directories outside the intended scope. **Example:** """cmake # Sanitizing a file path set(INPUT_FILE "${CMAKE_SOURCE_DIR}/data/input.txt") file(TO_CMAKE_PATH "${INPUT_FILE}" CMAKE_INPUT_FILE) file(REAL_PATH "${CMAKE_INPUT_FILE}" ABSOLUTE_INPUT_FILE) message(STATUS "Absolute path to input file: ${ABSOLUTE_INPUT_FILE}") # Example of preventing path traversal set(USER_PROVIDED_PATH "/path/to/safe/directory") # Assume this path is set from user input after validation! set(FILE_NAME "important.txt") # Correct way to construct a safe path set(SAFE_FILE_PATH "${USER_PROVIDED_PATH}/${FILE_NAME}") # Only IF USER_PROVIDED_PATH is validated and safe! message(STATUS "Safe file path: ${SAFE_FILE_PATH}") # DANGEROUS: Avoid direct concatenation without validation # set(UNSAFE_FILE_PATH "${USER_INPUT}/${FILE_NAME}") # POTENTIAL VULNERABILITY! """ ### Standard 1.3: Escape Shell Commands **Do This:** Use "string(REPLACE)" or "string(QUOTE)" to escape strings that are passed to "execute_process" or "add_custom_command" to prevent command injection. Prefer passing arguments as a list instead of a single string to "execute_process". **Don't Do This:** Construct shell commands by directly concatenating strings, as this makes your build process vulnerable to command injection. **Why:** Proper escaping ensures that user-provided strings are treated as data rather than executable code by the shell. **Example:** """cmake # Using string(QUOTE) to escape a string set(my_variable "value with spaces and \"quotes\"") string(QUOTE "${my_variable}" QUOTED_VARIABLE) message(STATUS "Quoted variable: ${QUOTED_VARIABLE}") # Outputs: "value with spaces and \"quotes\"" # Using execute_process with arguments as a list: MUCH SAFER set(MY_INPUT "potentially malicious input") execute_process( COMMAND my_command "${MY_INPUT}" RESULT_VARIABLE result OUTPUT_VARIABLE output ERROR_VARIABLE error ) # String replace example (less safe than list approach) set(UNTRUSTED_INPUT "data & rm -rf /") string(REPLACE "&" "\\&" ESCAPED_INPUT "${UNTRUSTED_INPUT}") execute_process(COMMAND /bin/sh -c "echo ${ESCAPED_INPUT}") # Still potentially dangerous; list approach PREFERRED. """ ## 2. Secure Dependency Management ### Standard 2.1: Use Version Pins and Integrity Checks **Do This:** When including external libraries or dependencies, always specify precise version numbers or commit hashes and verify integrity using checksums or signatures. Use CMake's FetchContent module with "GIT_TAG" or "URL_HASH" to ensure consistency. Consider using a package manager like Conan or vcpkg for dependency management. **Don't Do This:** Rely on unspecified versions or "latest" tags, as these can introduce vulnerabilities if dependencies are updated with malicious or flawed code. **Why:** Pinning versions and verifying integrity ensures that you are using known, trusted versions of your dependencies. **Example:** """cmake include(FetchContent) FetchContent_Declare( googletest GIT_REPOSITORY https://github.com/google/googletest.git GIT_TAG v1.11.0 #Pin to a specific version! ) FetchContent_MakeAvailable(googletest) # Example with URL_HASH (for downloaded archives) FetchContent_Declare( MyLib URL https://example.com/mylib-1.2.3.tar.gz URL_HASH SHA256=a1b2c3d4e5f6... # Checksum! ) FetchContent_MakeAvailable(MyLib) """ ### Standard 2.2: Review External Code **Do This:** Review the code of external dependencies, especially those that are not widely used or well-maintained, to identify potential security vulnerabilities. **Don't Do This:** Blindly trust all external dependencies without any scrutiny. **Why:** Reviewing external code helps you identify and mitigate vulnerabilities before they can be exploited. This is especially important for dependencies included directly into your project. **Example:** (This is conceptual - code review is a manual process). Assume a third-party library "untrusted_lib" is being used. 1. Examine the library's source code for any suspicious patterns, such as unbounded loops, unchecked buffer sizes, or direct network access. 2. Check for known vulnerabilities in the library using public vulnerability databases (e.g., CVE). 3. Ensure the library is actively maintained and security issues are promptly addressed. ### Standard 2.3: Use HTTPS for Downloads **Do This:** When using "file(DOWNLOAD)" or "FetchContent", always use "HTTPS" URLs to ensure secure transfer of files. **Don't Do This:** Use "HTTP" URLs, which are susceptible to man-in-the-middle attacks where downloaded files can be intercepted and modified. **Why:** HTTPS provides encryption and authentication, preventing attackers from tampering with downloaded files during transit. **Example:** """cmake # Correct: Using HTTPS file(DOWNLOAD "https://example.com/myfile.tar.gz" "${CMAKE_BINARY_DIR}/myfile.tar.gz" TLS_VERIFY ON) # Always verify TLS certificates # Incorrect: Using HTTP # file(DOWNLOAD "http://example.com/myfile.tar.gz" "${CMAKE_BINARY_DIR}/myfile.tar.gz") # VIOLATION """ ## 3. Compiler and Linker Security Features ### Standard 3.1: Enable Security Flags **Do This:** Enable compiler and linker security flags such as position-independent executable (PIE), stack smashing protection ("-fstack-protector-strong"), and address space layout randomization (ASLR). Use CMake's "CMAKE_CXX_FLAGS", "CMAKE_C_FLAGS", and "CMAKE_EXE_LINKER_FLAGS" to set these flags. Consider using "CheckCXXCompilerFlag" to conditionally add flags **Don't Do This:** Disable security flags or rely on default compiler settings, as this exposes your application to common vulnerabilities. **Why:** These flags harden your application against exploitation by making it more difficult for attackers to inject code or manipulate memory. **Example:** """cmake include(CheckCXXCompilerFlag) # Enable position-independent executable (PIE) set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -fPIE") set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -fPIE") # Enable stack smashing protection (strong) check_cxx_compiler_flag("-fstack-protector-strong" SUPPORTS_SSP) if(SUPPORTS_SSP) set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fstack-protector-strong") set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -fstack-protector-strong") endif() # Enable address space layout randomization (ASLR). Usually enabled by default but can be explicitly set # (This is often handled by the OS, but link flags can enforce it) set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -pie") #Note PIE is required for full ASLR. set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -pie") """ ### Standard 3.2: Use Sanitizers During Development **Do This:** Use compiler sanitizers (AddressSanitizer, MemorySanitizer, UndefinedBehaviorSanitizer) during development to detect memory errors and undefined behavior. Use "CMAKE_CXX_FLAGS_DEBUG" and "CMAKE_C_FLAGS_DEBUG" to enable sanitizers in debug builds. **Don't Do This:** Only test your code in release mode or without sanitizers, as this can mask critical memory bugs and security vulnerabilities. **Why:** Sanitizers can detect memory corruption, data races, and undefined behavior that can lead to security vulnerabilities. **Example:** """cmake # Enable AddressSanitizer (ASan) in debug builds if(CMAKE_BUILD_TYPE STREQUAL "Debug") set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -fsanitize=address") set(CMAKE_C_FLAGS_DEBUG "${CMAKE_C_FLAGS_DEBUG} -fsanitize=address") endif() # Enable UndefinedBehaviorSanitizer (UBSan) in debug builds if(CMAKE_BUILD_TYPE STREQUAL "Debug") set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -fsanitize=undefined") set(CMAKE_C_FLAGS_DEBUG "${CMAKE_C_FLAGS_DEBUG} -fsanitize=undefined") endif() # Enable MemorySanitizer (MSan) - Requires special setup in some compilers # if(CMAKE_BUILD_TYPE STREQUAL "Debug") # set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -fsanitize=memory") # set(CMAKE_C_FLAGS_DEBUG "${CMAKE_C_FLAGS_DEBUG} -fsanitize=memory") # endif() """ ### Standard 3.3: Enable RELRO (Relocation Read-Only) **Do This:** Enable RELRO (Relocation Read-Only) by including appropriate linker flags. Full RELRO is preferred. **Don't Do This:** Neglect to enable RELRO, as it provides significant protection against memory corruption exploits. **Why:** RELRO hardens the Global Offset Table (GOT) and Procedure Linkage Table (PLT) by making them read-only after program initialization, preventing attackers from overwriting them to redirect control flow. **Example:** """cmake # Enable RELRO (Full RELRO is preferred if supported by the linker) set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -Wl,-z,relro,-z,now") set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -Wl,-z,relro,-z,now") # Check if your linker supports these flags and adjust accordingly. """ ## 4. Privilege Management ### Standard 4.1: Avoid Running CMake with Elevated Privileges **Do This:** Run CMake with the least necessary privileges. Avoid running CMake as root or with administrative privileges unless absolutely required. **Don't Do This:** Assume elevated privileges are always necessary. **Why:** Running CMake with elevated privileges increases the risk that a vulnerability in the CMake scripts or build process will lead to system compromise. **Example:** Ensure that the user running CMake has only the necessary file system permissions to read source files, create build directories, and write output files. Avoid using "sudo" or running CMake as the "root" user unless required (e.g., for installation to system directories). Consider using containerization to isolate the build process. ### Standard 4.2: Restrict Installation Permissions **Do This:** When installing your project, set appropriate permissions on the installed files and directories to prevent unauthorized access or modification. **Don't Do This:** Install files with overly permissive permissions (e.g., 777) or allow them to be writable by unprivileged users. **Why:** Restricting installation permissions limits the potential impact of vulnerabilities or misconfigurations. **Example:** """cmake install(TARGETS my_executable DESTINATION bin PERMISSIONS OWNER_EXECUTE OWNER_READ GROUP_EXECUTE GROUP_READ WORLD_EXECUTE WORLD_READ) #755 install(DIRECTORY ${CMAKE_SOURCE_DIR}/conf DESTINATION etc/myapp PERMISSIONS OWNER_READ OWNER_WRITE GROUP_READ WORLD_READ) #644 #Use FILE_PERMISSIONS to change the actual files' permissions right before install. """ ## 5. Secure Configuration Files ### Standard 5.1: Protect Sensitive Information **Do This:** Protect sensitive information such as API keys, passwords, and cryptographic keys by storing them in secure configuration files or environment variables, and avoid hardcoding them directly in CMake scripts. Use CMake to read the values from a secure location. **Don't Do This:** Hardcode secrets in CMakeLists.txt or expose them in build scripts. **Why:** Hardcoded secrets can be easily discovered by attackers, leading to unauthorized access or data breaches. **Example:** """cmake # Reading an API key from an environment variable if(DEFINED ENV{API_KEY}) set(API_KEY "$ENV{API_KEY}" CACHE STRING "API key" FORCE) mark_as_advanced(API_KEY) # Prevents accidental exposure in GUI tools add_definitions(-DAPI_KEY="${API_KEY}") #Pass to compiler. else() message(FATAL_ERROR "API_KEY environment variable not set!") endif() # Reading a secret from a file (ensure file has restricted permissions) file(READ "${CMAKE_SOURCE_DIR}/secrets.txt" SECRET_VALUE) #Further sanitize and use SECRET_VALUE """ ### Standard 5.2: Secure Default Values **Do This:** Provide secure default values for configuration options to prevent misconfiguration that could lead to vulnerabilities. **Don't Do This:** Use insecure or empty default values. **Why:** Secure defaults ensure that your application is secure by design, even if users do not explicitly configure security settings. **Example:** """cmake option(ENABLE_SSL "Enable SSL encryption" ON) # Secure default if(ENABLE_SSL) message(STATUS "SSL encryption enabled.") add_definitions(-DENABLE_SSL) else() message(STATUS "SSL encryption disabled.") endif() option(ALLOW_ANONYMOUS_ACCESS "Allow anonymous access to the server" OFF) # Secure default # Further logic based on ALLOW_ANONYMOUS_ACCESS. """ ## 6. Continuous Security Testing ### Standard 6.1: Integrate Security Checks into CI/CD **Do This:** Integrate static analysis tools, vulnerability scanners, and fuzzers into your CI/CD pipeline to automatically detect security vulnerabilities in your CMake scripts and generated code. **Don't Do This:** Rely on manual security testing or perform security checks only at the end of the development cycle. **Why:** Automated security testing helps you identify vulnerabilities early and often, reducing the cost and effort required to fix them. **Example:** 1. Use a static analysis tool such as "cppcheck" or "clang-tidy" to identify potential security issues in your C++ code. 2. Use a vulnerability scanner such as "bandit" (for Python) or "brakeman" (for Ruby) to identify vulnerabilities in your CMake scripts. 3. Integrate fuzzing tools to test input handling. ### Standard 6.2: Regularly Update Dependencies **Do This:** Establish a process for regularly updating dependencies to incorporate security patches and bug fixes. **Don't Do This:** Neglect updates, as this leaves your application vulnerable to known exploits. **Why:** Keeping your dependencies up-to-date ensures that you are protected against the latest security threats. **Example:** Use a dependency management tool (Conan, vcpkg) that facilitates updates and provides notifications when new versions are available. ## 7. Using Modern CMake Effectively for Security ### Standard 7.1: Utilize Target-Based Security Properties **Do This:** Leverage CMake's target-based properties to apply security settings consistently across your project. For example, use "target_compile_options", "target_link_libraries", and "target_include_directories" to manage compiler flags, linker flags, and include paths for each executable and library. **Don't Do This:** Rely on global variables (like "CMAKE_CXX_FLAGS") for security-related settings, as their scope can be unclear and lead to inconsistencies. **Why:** Target-based properties provide a clear and maintainable way to manage security settings for individual components of your project. **Example:** """cmake add_library(mylibrary SHARED mylibrary.cpp) target_compile_options(mylibrary PRIVATE -Wall -Wextra -Werror # Treat warnings as errors -fstack-protector-strong ) target_link_options(mylibrary PRIVATE -Wl,-z,relro,-z,now ) """ ### Standard 7.2: Use Interface Libraries for Public Headers **Do This:** When creating libraries, use "INTERFACE" libraries to manage public headers and dependencies. This allows you to control which dependencies are exposed to downstream targets and prevent leakage of internal implementation details. **Don't Do This:** Expose all headers and dependencies of a library to downstream targets by default. **Why:** Interface libraries improve encapsulation and reduce the risk of exposing internal vulnerabilities to external code. """cmake add_library(MyLib INTERFACE) target_include_directories(MyLib INTERFACE $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include> $<INSTALL_INTERFACE:include> ) add_library(MyLibImpl SHARED MyLibImpl.cpp) target_link_libraries(MyLibImpl PRIVATE SomeOtherLib) #Internal dependency target_include_directories(MyLibImpl PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/internal) target_link_libraries(MyLib INTERFACE MyLibImpl) """ ### Standard 7.3: Be Careful with Generator Expressions for Security-Sensitive Settings **Do This:** Use generator expressions carefully in security-sensitive settings. While generator expressions can adapt to different environments, improper use can lead to unexpected behavior and security vulnerabilities. Ensure that any environment-dependent choices made via generator expressions are thoroughly validated and tested. **Don't Do This:** Add unchecked user input or potentially malicious content from the environment directly via generator expressions. **Why:** Generator expressions are evaluated at build time and can introduce vulnerabilities if not used carefully, especially when dealing with user-provided data or environment variables. **Example:** """cmake add_executable(my_app main.cpp) target_compile_definitions(my_app PRIVATE $<$<CONFIG:Debug>:DEBUG_MODE> #Conditional Debug macro. ) #Potentially dangerous: #target_compile_definitions(my_app PRIVATE $<CONFIG:${SOME_ENV_VAR}:DANGEROUS) #AVOID. """ By following these security best practices, developers can create more secure CMake configurations and applications, reducing the risk of vulnerabilities and protecting sensitive data. This guide provides a comprehensive foundation for secure CMake development, but it is essential to stay updated with the latest security threats and best practices as they evolve.
# Performance Optimization Standards for CMake This document outlines best practices for optimizing CMake build configuration files to improve build times, reduce resource consumption, and enhance overall development workflow performance. These guidelines focus on using modern CMake features (CMake 3.15+) and avoiding common pitfalls. ## 1. Minimize Included Files and Dependencies The first principle of efficient CMake configurations is to reduce the amount of parsing and processing CMake needs to do. This primarily revolves around limiting the number of files included and keeping the dependency tree lean. ### 1.1. Reducing the Number of "include()" Calls **Do This:** Limit the number of "include()" calls. Refactor common functionality into modules that are only included when needed by specific targets. **Don't Do This:** Avoid blanket "include()" statements at the top level of your "CMakeLists.txt". This forces CMake to parse irrelevant files for every target. **Why:** Unnecessary "include()" calls introduce overhead by forcing CMake to parse and execute code that may not be relevant to the current target. **Example:** """cmake # Anti-pattern: including everything globally # include(Utilities) # Parses Utilities.cmake even if not needed # Better pattern: Target-specific inclusion function(add_my_library name) add_library(${name} ...) target_sources(${name} ...) # Only include Utilities if this target needs it. Utilies.cmake can expose # cmake functions in this scenario that are only available for targets that include it. include(Utilities OPTIONAL) if(DEFINED Utilities_FOUND) # Call Utility functions utility_function(${name}) endif() endfunction() add_my_library(MyLibrary) """ ### 1.2. Interface Libraries for Header-Only Dependencies **Do This:** Use "INTERFACE" libraries to propagate header-only dependencies and preprocessor definitions. **Don't Do This:** Copy-paste compiler flags and include directories across multiple targets. **Why:** "INTERFACE" libraries provide a clean and efficient way to manage dependencies without requiring target linking or source code compilation. This avoids unnecessary rebuilds when only header files change. **Example:** """cmake # Create an interface library for a header-only dependency add_library(MyHeaderOnlyLib INTERFACE) # Specify the include directories needed by MyHeaderOnlyLib target_include_directories(MyHeaderOnlyLib INTERFACE $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include> $<INSTALL_INTERFACE:include> ) # Optionally, add preprocessor definitions target_compile_definitions(MyHeaderOnlyLib INTERFACE MY_HEADER_ONLY_LIB_ENABLED ) # Link against this interface library add_executable(MyExecutable main.cpp) target_link_libraries(MyExecutable PRIVATE MyHeaderOnlyLib) """ ### 1.3. Package Handling and Dependencies **Do This:** Use "find_package()" with the "CONFIG" mode and version requirements. Leverage CMake packages fully. **Don't Do This:** Manually specify include paths and library locations when a proper CMake package exists. **Why:** "find_package()" with "CONFIG" mode offloads dependency resolution to the imported package's CMake configuration files, often providing optimized build settings. This also allows cleaner cross-platform support. Modern CMake makes consumption of 3rd party libraries significantly easier using targets. **Example:** """cmake # Correct: Using find_package with required version and COMPONENTS find_package(Boost 1.70 REQUIRED COMPONENTS system filesystem) if (Boost_FOUND) add_executable(MyProgram main.cpp) target_link_libraries(MyProgram Boost::system Boost::filesystem) #Use IMPORTED targets endif() # Anti-pattern: Manual specification (prone to errors and platform-specific issues) # include_directories(/path/to/boost/include) # link_libraries(/path/to/boost/lib/libboost_system.a) """ ### 1.4 Conditional Dependencies **Do This:** Make dependencies optional and conditional using "find_package()" with the "REQUIRED" keyword omitted. Consider using components with "find_package" to minimize extraneous dependencies **Don't Do This:** Force hard dependencies when a feature can degrade gracefully without them **Why:** Reducing the number of mandatory dependencies lightens the dependency closure and improves configuration time, especially when some dependencies involve significant external processing during the CMake stage (e.g., external project inclusion or code generation). **Example:** """cmake find_package(OptionalDependency) if(OptionalDependency_FOUND) # Link to the optional dependency target_link_libraries(MyTarget PUBLIC OptionalDependency::OptionalDependency) # Note imported namespace target # ... use optional dependency features else() message(STATUS "OptionalDependency not found, some features will be disabled") # ... disable features that depend on the optional dependency target_compile_definitions(MyTarget PRIVATE "NO_OPTIONAL_DEPENDENCY") endif() """ ## 2. Optimize Compiler and Linker Flags Strategic selection and use of compiler and linker flags can drastically improve code generation, link times, and runtime performance. ### 2.1. Position Independent Executables (PIE) **Do This:** Enable Position Independent Executables (PIE) for executables, especially on modern Linux systems, and use Address Space Layout Randomization (ASLR). **Don't Do This:** Disable PIE or ASLR without a strong, justified reason. **Why:** PIE and ASLR enhance security by making it harder for attackers to exploit memory corruption vulnerabilities. **Example:** """cmake set(CMAKE_POSITION_INDEPENDENT_CODE ON) # Enable for all targets # OR, target-specific set_target_properties(MyExecutable PROPERTIES POSITION_INDEPENDENT_CODE ON ) """ ### 2.2. Link-Time Optimization (LTO) **Do This:** Enable LTO for release builds to allow the compiler to perform optimizations across translation units. **Don't Do This:** Use LTO indiscriminately for debug builds, as it can significantly increase compile times. **Why:** LTO can yield significant performance improvements by enabling inter-procedural optimizations, dead code elimination, and function inlining across the entire program. **Example:** """cmake # Enable LTO for Release builds if (CMAKE_BUILD_TYPE STREQUAL "Release") set(CMAKE_INTERPROCEDURAL_OPTIMIZATION CHECKED) #CHECKED allows user overwrite via ccmake or cmake-gui endif() """ ### 2.3. Optimize for Target Architecture **Do This:** Set compiler flags to optimize code for the target CPU architecture (e.g., "-march=native" for GCC/Clang). Use CMake generator expressions to set architecture specific flags, for example, when cross compiling. **Don't Do This:** Use generic optimization flags that don't take advantage of specific CPU features. **Why:** Architecture-specific optimizations can improve performance by utilizing instruction sets and features available on the target CPU. **Example:** """cmake if(CMAKE_SYSTEM_PROCESSOR MATCHES "x86_64") target_compile_options(MyTarget PRIVATE "-march=native") # Optimize for the build machine's CPU elseif(CMAKE_SYSTEM_PROCESSOR MATCHES "arm64") target_compile_options(MyTarget PRIVATE "-march=armv8-a") endif() """ ### 2.4. Precompiled Headers (PCH) **Do This:** Use precompiled headers to reduce compilation time by pre-compiling common header files. **Don't Do This:** Neglect using PCHs on large codebases with slow includes. **Why:** PCH avoids redundant compilation of frequently included headers, significantly reducing build times. **Example:** """cmake # Create a precompiled header set_source_files_properties(MyProject/pch.h PROPERTY COMPILE_LANGUAGE CXX) add_library(MyProjectPCH OBJECT MyProject/pch.h) target_compile_options(MyProjectPCH PRIVATE -x c++-header) # Adjust for your compiler # Use the precompiled header target_precompile_headers(MyTarget PUBLIC MyProject/pch.h) # Requires CMake >= 3.16 #alternative for CMake < 3.16: #target_sources(MyTarget PRIVATE MyProject/source1.cpp MyProject/source2.cpp) #set_source_files_properties( # ${CMAKE_CURRENT_SOURCE_DIR}/source1.cpp # ${CMAKE_CURRENT_SOURCE_DIR}/source2.cpp # PROPERTIES # COMPILE_FLAGS "-include pch.h" #) """ ## 3. Efficient CMake Code Structures The structure and organization of CMake code directly impact build times. Avoiding unnecessary operations and using efficient constructs reduces the configuration overhead. ### 3.1. Minimize String Operations **Do This:** Avoid extensive string manipulation within CMake scripts. For example avoid excessive generation of paths and lists. **Don't Do This:** Perform complex string processing that will be invoked on every configuration. **Why:** CMake string operations can be slow, especially when dealing with large strings or complex regexes. **Example:** """cmake # Bad: Repeated string appending/manipulation set(MY_PATH "${CMAKE_SOURCE_DIR}") set(MY_PATH "${MY_PATH}/src") set(MY_PATH "${MY_PATH}/module1") # Better: Directly construct the path if possible. set(MY_PATH "${CMAKE_SOURCE_DIR}/src/module1") """ ### 3.2. Use "set(CACHE)" Sparingly **Do This:** Only use "set(CACHE)" for variables that need to persist across CMake invocations, and use them BEFORE "project()" **Don't Do This:** Overuse "set(CACHE)", as it can slow down the configuration process. **Why:** Cache variables are re-read from disk on every CMake run, which incurs overhead. Limit their use to persistent configuration options. **Example:** """cmake # Good pattern: Defining cache variables before project() set(MY_OPTION "default_value" CACHE STRING "Description of the option") project(MyProject) # Within a function or scope within CMake set(LOCAL_VARIABLE "value") # Not cached """ ### 3.3. Efficient List Operations **Do This:** Use CMake's built-in list manipulation commands efficiently. Only iterate when necessary. **Don't Do This:** Iterate over lists unnecessarily or perform redundant list operations. **Why:** Inefficient list handling degrades CMake performance, especially on large projects with many source files or dependencies. **Example:** """cmake # Bad: Inefficient loop set(MY_FILES "file1.cpp;file2.cpp;file3.cpp") foreach(FILE ${MY_FILES}) message(STATUS "Processing ${FILE}") # Unnecessarily iterates endforeach() # Better: Process the entire list at once when possible message(STATUS "All files: ${MY_FILES}") """ ### 3.4. Variable Scoping **Do This:** Leverage variable scoping (functions, blocks) to avoid variable pollution in the global scope. **Don't Do This:** Use global variables unnecessarily, as these can lead to conflicts and unintended side effects, slowing down debugging and adding complexity. **Why:** Scoping improves code clarity, reduces the risk of name collisions, and allows CMake to manage variable lifetimes more efficiently. **Example:** """cmake function(my_function) set(local_variable "value") # Local to the function message(STATUS "Local variable: ${local_variable}") endfunction() my_function() #message(STATUS "Local variable outside function: ${local_variable}") # Error: variable not defined """ ### 3.5 Generator Expressions **Do This:** Use Generator expressions to defer complex logic to generator time. This is especially crucial if the dependency checking involves many checks. **Don't Do This:** Perform potentially costly logic evaluation during CMake configuration when the information is only needed at generate time. **Why:** Generator expressions are evaluated during build system generation, so it can evaluate system information later during generate step rather than at configuration. **Example:** """cmake add_executable(my_app main.cpp) target_compile_definitions(my_app PRIVATE "DEBUG_MODE=$<$<CONFIG:Debug>:TRUE;FALSE>" # Evaluated during compile step rather than evaluate the if statement in CMake config ) """ ## 4. Parallel Builds and Ninja Generator Embrace parallel builds and modern build systems to reduce compilation time. ### 4.1. Utilizing Parallel Compilation **Do This:** Encourage users to use the "-j" flag with "make" or specify the number of jobs in IDEs to enable parallel compilation. **Don't Do This:** Artificially limit the number of parallel jobs or fail to inform users about the benefits of parallel compilation. **Why:** Parallel compilation significantly reduces build times, especially on multi-core processors. **Example:** Instruct users to build with: "make -j$(nproc)" ### 4.2. Ninja Generator **Do This:** Use the Ninja generator, especially for large projects, as it is generally faster than the "make" generator. **Don't Do This:** Stick with the "make" generator without considering the potential performance benefits of Ninja. **Why:** Ninja is designed for speed, with a focus on incremental builds and parallel execution. It tends to outperform "make" in many scenarios. **Example:** """bash cmake -G Ninja . ninja """ ### 4.3. Ccache Integration **Do This:** Integrate "ccache" to speed up recompilation by caching compiled object files. **Don't Do This:** Ignore "ccache" in environments where frequent recompilation occurs. **Why:** "ccache" reduces build times by reusing previously compiled object files when the source code and compiler flags haven't changed. **Example:** """cmake # Add ccache to the compiler command line (requires ccache to be installed) find_program(CCACHE_PROGRAM ccache) if(CCACHE_PROGRAM) set_property(GLOBAL PROPERTY RULE_LAUNCH_COMPILE "${CCACHE_PROGRAM}") endif() """ ## 5. Modularization and Abstraction Breaking CMake configurations into logical modules simplifies maintenance and promotes reuse. ### 5.1. Custom Modules and Functions **Do This:** Refactor common build logic into reusable custom modules (".cmake" files) and functions. **Don't Do This:** Duplicate CMake code across multiple "CMakeLists.txt" files. **Why:** Modularization makes CMake configurations easier to understand, maintain, and extend and reduces parsing load to increase CMake configuration time. **Example:** """cmake # MyModule.cmake function(add_my_executable name) add_executable(${name} ${ARGN}) target_compile_features(${name} PUBLIC cxx_std_17) endfunction() # CMakeLists.txt include(MyModule) add_my_executable(MyProgram main.cpp) """ ### 5.2. Abstract Configuration Options **Do This:** Abstract complex configuration options behind simple, user-friendly variables with meaningful defaults. **Don't Do This:** Expose internal implementation details directly to the user. **Why:** Abstraction simplifies the configuration process, reduces the risk of errors, and allows you to change the underlying implementation without breaking user scripts. **Example:** """cmake # Good: Abstracted option option(ENABLE_FEATURE "Enable the super cool feature" ON) # Bad: Exposing implementation details # set(FEATURE_IMPL "internal_lib;dependency1;dependency2") # Hard to understand/maintain """ ### 5.3. Policy Management **Do This:** Explicitly set CMake policies using "cmake_policy()" to ensure consistent behavior across CMake versions and avoid deprecation warnings. **Don't Do This:** Rely on default policy settings, as these may change in future CMake versions. **Why:** Setting policies explicitly ensures that your CMake code behaves predictably and is forward-compatible. **Example:** """cmake cmake_minimum_required(VERSION 3.15) # Set the minimum required version before setting policies cmake_policy(SET CMP0077 NEW) # Example policy (use actual policy ID) """ By adhering to these coding standards, you can create CMake build configuration files that are performant, maintainable, and robust. This leads to faster build times, a smoother development workflow, and improved overall software quality. Remember to keep abreast of the latest CMake features and best practices by regularly consulting the official CMake documentation.
# Deployment and DevOps Standards for CMake This document outlines CMake coding standards focused on deployment and DevOps aspects. It provides guidelines for structuring CMake projects to facilitate robust build processes, seamless integration with CI/CD pipelines, and optimized production deployments. These standards are geared towards modern CMake practices and emphasize best practices for maintainability, performance, and security. ## 1. Build Process Standardization ### 1.1. Consistent Build Types **Standard:** Enforce consistent build types across development, testing, and production environments. * **Do This:** Use "CMAKE_BUILD_TYPE" and a consistent set of flags (e.g., "-DCMAKE_BUILD_TYPE=Release", "-DCMAKE_BUILD_TYPE=Debug") * **Don't Do This:** Hardcode compilation flags directly within "CMakeLists.txt" without considering build types. **Why:** Ensures predictable behavior and avoids discrepancies between environments. **Example:** """cmake # Set default build type if not specified if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) set(CMAKE_BUILD_TYPE Release CACHE STRING "Choose the type of build, options are: Debug Release RelWithDebInfo MinSizeRel." FORCE) endif() # Compiler flags based on build type if(CMAKE_BUILD_TYPE STREQUAL "Debug") message(STATUS "Debug build type selected") add_compile_options(-g -O0) elseif(CMAKE_BUILD_TYPE STREQUAL "Release") message(STATUS "Release build type selected") add_compile_options(-DNDEBUG -O3) endif() """ ### 1.2. Out-of-Source Builds **Standard:** Always use out-of-source builds to prevent polluting source directories. * **Do This:** Create a separate build directory (e.g., "build", "out"). Run CMake from this directory. * **Don't Do This:** Run CMake directly in the source directory. **Why:** Keeps the source tree clean and allows for multiple independent builds with different configurations. **Example:** """bash mkdir build cd build cmake .. make """ ### 1.3. Package Management Integration **Standard:** Integrate with package managers (e.g., Conan, vcpkg) for dependency handling. * **Do This:** Use "find_package" with package manager configuration files. Consider using CMake FetchContent for simple external dependencies. * **Don't Do This:** Manually download and include dependencies directly in the source tree. **Why:** Simplifies dependency management, improves reproducibility, and enables version control of dependencies. **Example (Using FetchContent):** """cmake include(FetchContent) FetchContent_Declare( googletest GIT_REPOSITORY https://github.com/google/googletest.git GIT_TAG v1.13.0 ) # For versions of CMake older than 3.14, use FetchContent_GetProperties # instead of FetchContent_Populate FetchContent_Populate(googletest) if(NOT googletest_SOURCE_DIR) FetchContent_MakeAvailable(googletest) endif() include_directories(${googletest_SOURCE_DIR}/googletest/include) add_executable(my_tests test.cpp) target_link_libraries(my_tests GTest::gtest_main) """ ### 1.4. Minimizing External Dependencies **Standard:** Keep external dependencies to a minimum, preferring standard library features where appropriate. * **Why:** This reduces the risk of dependency conflicts, build failures, and security vulnerabilities. It also improves build times and reduces the overall complexity of the deployment process. * **Do This:** Carefully evaluate the necessity of each dependency. Consider writing small, self-contained functions to replace external libraries where feasible. * **Don't Do This:** Add dependencies without a clear justification. Assume that a dependency provides better performance or functionality without benchmarking and profiling. ## 2. CI/CD Integration ### 2.1. Configuration-as-Code **Standard:** Define build and deployment configurations in code (e.g., YAML files, shell scripts). * **Do This:** Store CI/CD configurations (e.g., ".gitlab-ci.yml", "Jenkinsfile") alongside the "CMakeLists.txt". * **Don't Do This:** Manually configure CI/CD pipelines through web interfaces. **Why:** Enables version control, auditability, and reproducibility of the entire build and deployment process. **Example (.gitlab-ci.yml):** """yaml stages: - build - test - deploy build: stage: build script: - mkdir build - cd build - cmake .. - make test: stage: test dependencies: - build script: - cd build - ./my_tests deploy: stage: deploy dependencies: - test script: - echo "Deploying application..." - # Deployment commands here only: - main """ ### 2.2. Test Automation **Standard:** Automate testing as part of the CI/CD pipeline. * **Do This:** Use "add_test" and CTest to define and run tests. * **Don't Do This:** Rely solely on manual testing. **Why:** Ensures code quality, detects regressions early, and provides confidence in deployments. **Example:** """cmake enable_testing() add_executable(my_tests test.cpp) target_link_libraries(my_tests gtest_main) include(GoogleTest) gtest_discover_tests(my_tests) #Recommended for modern CMake. Will auto-detect tests. add_test(NAME MyTest COMMAND my_tests) #Legacy method of adding tests """ ### 2.3. Artifact Management **Standard:** Manage build artifacts (e.g., executables, libraries) using version control and artifact repositories. * **Do This:** Use staging directories for artifacts, then upload to an artifact repository (e.g., Artifactory, Nexus) during CI/CD. * **Don't Do This:** Directly deploy from the build environment without versioning artifacts. **Why:** Enables rollback capabilities, provides a central location for binary sharing, and facilitates auditing. **Example (CMake for packaging):** """cmake include(InstallRequiredSystemLibraries) include(GNUInstallDirs) #Recommended modern approach for finding standard install locations install(TARGETS my_executable DESTINATION ${CMAKE_INSTALL_BINDIR}) # Use CMAKE_INSTALL_BINDIR for /usr/local/bin install(DIRECTORY assets/ DESTINATION ${CMAKE_INSTALL_DATADIR}/my_application) # Use CMAKE_INSTALL_DATADIR for /usr/local/share """ ### 2.4. Conditional Compilation for Environments **Standard:** Use conditional compilation and preprocessor directives to configure code behavior based on the target environment (development, testing, production). * **Do This:** Define macros or variables based on the environment and use them in the code. For example, use "-DDEBUG" or "-DPRODUCTION" flags passed to the compiler. * **Don't Do This:** Hardcode environment-specific configurations directly into the code without any mechanism for conditional behavior. **Why:** Allows for customized behavior like verbose logging in development or optimized performance in production. **Example:** CMakeLists.txt """cmake if (CMAKE_BUILD_TYPE STREQUAL "Debug") add_definitions(-DDEBUG) else() add_definitions(-DNDEBUG) endif() """ C++ Code: """c++ #ifdef DEBUG std::cout << "Debug message: Value = " << value << std::endl; #endif """ ## 3. Production Considerations ### 3.1. Optimization Flags **Standard:** Use appropriate optimization flags for production builds. * **Do This:** Use "-O3 -DNDEBUG" for Release builds. Profile the application and tune optimization flags accordingly. Consider using link-time optimization (LTO). * **Don't Do This:** Use "-g -O0" flags in production. **Why:** Improves performance and reduces binary size in production. **Example:** """cmake if(CMAKE_BUILD_TYPE STREQUAL "Release") add_compile_options(-O3 -DNDEBUG) endif() """ ### 3.2. Static Analysis **Standard:** Integrate static analysis tools (e.g., Clang Static Analyzer, SonarQube) into the CI/CD pipeline. * **Do This:** Run static analysis on every commit and fail the build if critical issues are found. Address findings promptly. * **Don't Do This:** Ignore static analysis warnings. **Why:** Identifies potential bugs, security vulnerabilities, and code quality issues early in the development lifecycle. Makes sure you have a clean build that's ready for production. **Example (basic clang-tidy):** """cmake find_program(CLANG_TIDY NAMES clang-tidy) if(CLANG_TIDY) set_property(SOURCE ${SOURCES} PROPERTY CXX_CLANG_TIDY "${CLANG_TIDY}") message(STATUS "clang-tidy found: ${CLANG_TIDY}") else() message(WARNING "clang-tidy not found.") endif() """ ### 3.3. Minimizing Binary Size **Standard:** Reduce the size of the final binary for production deployments. * **Do This:** Enable link-time optimization (LTO), strip debugging symbols, and use appropriate compiler flags. Remove unused code and dependencies. * **Don't Do This:** Include unnecessary debugging information or unused libraries in production binaries. **Why:** Smaller binaries can be deployed more quickly, consume less storage space, and potentially improve load times. **Example (LTO):** """cmake set(CMAKE_INTERPROCEDURAL_OPTIMIZATION TRUE) # Enables LTO for Release and RelWithDebInfo """ ### 3.4 Security Hardening **Standard:** Implement security hardening measures. * **Do This:** Enable compiler flags like "-fstack-protector-strong", "-D_FORTIFY_SOURCE=2", and use address sanitizers during testing. Regularly audit dependencies for vulnerabilities. Ensure proper user permissions. * **Don't Do This:** Deploy with default configurations and without security considerations. **Why:** Protects against common security exploits such as buffer overflows and format string vulnerabilities. **Example:** """cmake if(CMAKE_BUILD_TYPE STREQUAL "Release") add_compile_options( -fstack-protector-strong -D_FORTIFY_SOURCE=2 ) add_link_options(-Wl,-z,relro,-z,now) #Enable RELRO and NOW endif() """ ### 3.5. Resource Management **Standard:** Implement strategies for effective resource management (CPU, Memory, Disk I/O) * **Do This:** Use resource limits, profiling tools to optimize bottlenecks, and efficient data structures to minimize memory usage. Implement proper error handling and resource cleanup procedures. * **Don't Do This:** Leak resources, perform excessive I/O operations, or fail to monitor resource consumption in production. **Why:** Ensures that the application runs reliably and efficiently. Reduces risk of resource exhaustion and improves overall system stability. ## 4. Modern CMake and DevOps ### 4.1. Using Modules Effectively **Standard:** Leverage CMake's module system for code reuse and maintainability. * **Do This:** Create custom modules for common tasks and project-specific logic. Consider contributing reusable modules to the CMake community. * **Don't Do This:** Duplicate code across multiple "CMakeLists.txt" files. **Why:** Promotes modularity, reduces code duplication, and improves maintainability. **Example (Creating a custom module):** """cmake # In Modules/MyProjectHelpers.cmake function(my_custom_function target) # ... implementation ... endfunction() """ """cmake # In CMakeLists.txt include(MyProjectHelpers) my_custom_function(my_target) """ ### 4.2. Export Targets **Standard:** Use "install(TARGETS)" and "export" commands to create easily consumable packages. * **Do This:** Export targets to allow other projects to easily link against your libraries. * **Don't Do This:** Require consumers to manually find and link against dependencies. **Why:** Simplifies dependency management and promotes code reuse. **Example:** """cmake install(TARGETS my_library EXPORT my_library_export DESTINATION lib) install( EXPORT_SETTING_FILE DESTINATION lib/cmake/MyLibrary EXPORT_NAME MyLibraryTargets ) # In CMakeLists.txt of consuming project: find_package(MyLibrary REQUIRED) target_link_libraries(my_consumer MyLibrary::my_library) """ ### 4.3. Toolchain Files **Standard:** Utilize toolchain files for cross-compilation and specialized environments. * **Do This:** Create toolchain files to specify the compiler, linker, and other tools for a specific target platform. * **Don't Do This:** Hardcode compiler paths and flags directly in "CMakeLists.txt". **Why:** Enables cross-compilation and simplifies building for different target architectures. **Example (Toolchain file - my_toolchain.cmake):** """cmake set(CMAKE_SYSTEM_NAME Generic) set(CMAKE_SYSTEM_PROCESSOR arm) set(CMAKE_C_COMPILER arm-linux-gnueabi-gcc) set(CMAKE_CXX_COMPILER arm-linux-gnueabi-g++) set(CMAKE_FIND_ROOT_PATH /opt/arm-linux-gnueabi) set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY) #Usage: cmake -DCMAKE_TOOLCHAIN_FILE=my_toolchain.cmake .. """ ### 4.4. Handling Different Operating Systems **Standard:** Use CMake commands to handle differences in operating systems and compiler versions gracefully. * **Do This:** Use "if(WIN32)", "if(UNIX)", and similar checks, along with "CMAKE_CXX_COMPILER_ID", but prefer feature detection where possible. * **Don't Do This:** Use platform-specific code without proper checks. **Why:** Ensures the project can be built and deployed correctly across different environments. **Example:** """cmake if(WIN32) add_definitions(-D_WIN32) endif() if(CMAKE_CXX_COMPILER_ID MATCHES "Clang") message(STATUS "Using Clang compiler") endif() """ ## 5. Anti-Patterns and Common Mistakes ### 5.1. Over-Reliance on "execute_process" **Anti-Pattern:** Excessive use of "execute_process" for tasks that CMake can handle natively. * **Why:** Reduces portability, increases complexity, and can introduce security vulnerabilities. "execute_process" should be a last resort. * **Alternatives:** Use CMake's built-in commands for file manipulation, dependency management, and build system generation. ### 5.2. Ignoring CMAKE_EXPORT_COMPILE_COMMANDS **Anti Pattern:** Not setting "CMAKE_EXPORT_COMPILE_COMMANDS" to "TRUE". * **Why:** Prevents IDEs and code analysis tools from properly indexing code, thus hindering development effectiveness. * **Solution:** Set "set(CMAKE_EXPORT_COMPILE_COMMANDS TRUE)" to generate compilation databases to give IDEs access to flags and compile definitions necessary for proper code analytics and code completion. ### 5.3. Complex Custom Commands **Anti-Pattern:** Creating overly complex custom commands. * **Why:** They can become difficult to maintain and debug. They can severely slow down the build if executed unnecessarily. * **Solution:** Consider refactoring them into CMake functions or scripts. Use DEPENDS clauses to ensure correct execution order. ### 5.4. Neglecting Documentation **Anti-Pattern:** Lack of documentation for CMake code. * **Why:** Makes it difficult for other developers to understand and maintain the build system. * **Solution:** Add comments to explain the purpose of CMake code, especially complex logic. Use Markdown to document the project structure and build process. ### 5.5 Using "MESSAGE" for important information or warnings. * **Why:** "Message" statements work, but are not as effective as built-in methods. * **Solution:** Utilize "STATUS", "WARNING", and "FATAL_ERROR" message types to clearly communicate the severity and intent of messages. This document provides a foundational set of standards and best practices for deployment and DevOps with CMake. Following these guidelines will lead to more robust, maintainable, and secure projects. Remember to adapt these standards to your specific project requirements and technology stack.
# Code Style and Conventions Standards for CMake This document outlines the code style and conventions to be followed when writing CMake code. Adhering to these standards will improve the readability, maintainability, and consistency of our CMake scripts. These standards are designed to leverage modern CMake features and avoid common pitfalls. ## 1. Formatting Consistent code formatting is crucial for readability. CMake, while flexible, benefits greatly from a structured layout. ### 1.1 Indentation * **Do This:** Use 2 spaces for indentation. Avoid tabs. * **Why:** Consistent indentation improves readability across different editors and environments. Two spaces provide a good balance between nesting visibility and line length. """cmake if(TARGET_FOUND) message(STATUS "Found the target.") target_include_directories(${PROJECT_NAME} PUBLIC "${TARGET_INCLUDE_DIR}") endif() """ * **Don't Do This:** Use tabs or inconsistent indentation. * **Why:** Tabs can be interpreted differently by different editors, leading to inconsistent formatting. ### 1.2 Line Length * **Do This:** Limit lines to a maximum of 120 characters. * **Why:** This improves readability, especially on smaller screens. * **How:** Break long lines using parentheses for function arguments or string concatenation where appropriate. """cmake target_link_libraries( ${PROJECT_NAME} PUBLIC ${DEPENDENCY_A} ${DEPENDENCY_B} ${DEPENDENCY_C} # Keep each dependency on its own line for better readability ) set(LONG_STRING "This is a very long string that needs to be broken " "into multiple lines for readability." ) """ ### 1.3 Whitespace * **Do This:** Use whitespace to improve readability. * **Why:** Proper spacing makes the code easier to scan and understand. * **Specific Rules:** * Add a space after commas in lists. * Add a space around operators (e.g., "SET(VAR ${VALUE})", not "SET(VAR ${VALUE})"). * Add an empty line between logical blocks of code (e.g., between variable definitions and target declarations). """cmake set(CMAKE_CXX_STANDARD 17) # Space after keyword, space around value add_executable(${PROJECT_NAME} main.cpp) # Separate logical blocks with an empty line target_link_libraries(${PROJECT_NAME} PUBLIC some_library) """ * **Don't Do This:** Omit spaces or use excessive spacing. ### 1.4 Comments * **Do This:** Use comments to explain complex logic, non-obvious choices, or the "why" behind a decision. * **Why:** Comments help others (and your future self) understand the code's purpose and intent. * Use "#" for single-line comments. * Use block comments ("#[[ ... ]]") for longer explanations or to temporarily disable code blocks """cmake # This function configures the project based on the platform. function(configure_platform) if(WIN32) # Windows-specific configuration message(STATUS "Configuring for Windows") else() # Linux/macOS configuration message(STATUS "Configuring for non-Windows") endif() endfunction() #[[ This entire section is temporarily disabled for debugging. message("This message will not be printed.") #]] """ * **Don't Do This:** Write obvious comments (e.g., "# Set variable x to 5"). Over-commenting can clutter the code. Also avoid commenting out code permanently; prefer deleting it. ## 2. Naming Conventions Clear and consistent naming conventions are essential for understanding the purpose of variables, functions, and targets. ### 2.1 Variables * **Do This:** Use uppercase for variables. Use underscores to separate words. * **Why:** This makes variables easily distinguishable from CMake keywords and commands. * "PROJECT_NAME", "CMAKE_BUILD_TYPE", "MY_CUSTOM_VARIABLE" * **Scope Awareness:** * Use prefixes to indicate scope when necessary (e.g., "LOCAL_VAR" for function-local variables, "PRIVATE_VAR" for internal implementation details). * Consider using "_" prefix to indicate a variable meant to implement behavior and not exposed as public interface. """cmake function(my_function) set(LOCAL_VAR "function-local" PARENT_SCOPE) # function value accessible outside the function endfunction() set(_HIDDEN_INTERNAL_VAR "very secret") """ * **Don't Do This:** Use lowercase or mixed-case for variables. Use cryptic or single-letter variable names (except for loop iterators). ### 2.2 Functions and Macros * **Do This:** Use lowercase for the function/macro name. Use underscores to separate words. * **Why:** Improves readability and distinguishes from variables. * "configure_project", "add_custom_target" * **Don't Do This:** Use uppercase or mixed-case. ### 2.3 Targets * **Do This:** Use camelCase for target names. * **Why:** Consistency across projects. * "myExecutable", "myLibrary" * **Prefixing:** * Prefix user defined targets. * "myExecutable", "myLibrary" * Don't prefix imported targets * "Boost::boost" * **Don't Do This:** Use uppercase or inconsistent casing. ### 2.4 Properties * **Do This:** Follow the existing CMake property naming convention (uppercase, underscores). * **Why:** Consistency with built-in properties. * "CMAKE_CXX_STANDARD", "BUILD_SHARED_LIBS" * **Don't Do This:** Deviate from the standard CMake property naming convention. ## 3. Command Usage Using CMake commands effectively is crucial for writing clean and maintainable scripts. ### 3.1 Modern CMake * **Do This:** Use modern CMake features (version 3.15+). * **Why:** Modern CMake provides more expressive and maintainable ways to manage targets, dependencies, and build configurations. * **Target-Based Approach:** Prefer target-based commands like "target_include_directories", "target_link_libraries", and "target_compile_features" over global commands like "include_directories", "link_libraries", and "add_definitions". """cmake # Modern CMake (Target-based) add_library(myLibrary STATIC library.cpp) target_include_directories(myLibrary PUBLIC include) target_link_libraries(myLibrary PUBLIC dependencyA dependencyB) target_compile_features(myLibrary PUBLIC cxx_std_17) # Add standards to target """ """cmake # Legacy CMake (Avoid) add_library(myLibrary STATIC library.cpp) include_directories(include) link_libraries(dependencyA dependencyB) set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++17") #Set compiler flags for all targets. """ * **Interface Libraries** Use interface libraries to propagate properties to dependents. """cmake add_library(MyLib INTERFACE) target_include_directories(MyLib INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}/include) target_compile_definitions(MyLib INTERFACE DEBUG_MODE) add_library(MyCode code.cpp) target_link_libraries(MyCode MyLib) # MyCode now has include directories and compile defines """ * **Don't Do This:** Use deprecated commands or variables. Rely on global state or side effects. Avoid mixing modern and legacy approaches. ### 3.2 Conditionals * **Do This:** Use "if()", "elseif()", and "else()" for conditional logic. Use "if(DEFINED ...)" or "if(NOT DEFINED ...)" to check if a variable is defined. Use comparison operators for string or numerical comparisons. Use "if(TARGET ...)" to check if a target exists. """cmake if(CMAKE_BUILD_TYPE STREQUAL "Debug") message(STATUS "Debug mode is enabled") elseif(CMAKE_BUILD_TYPE STREQUAL "Release") message(STATUS "Release mode is enabled") else() message(STATUS "Unknown build type") endif() if(TARGET myTarget) message(STATUS "Target myTarget exists") endif() """ * **Don't Do This:** Use deprecated conditional constructs (e.g., "IF(DEFINED ...)" - case-insensitive, error-prone). Use string comparisons for numerical values (CMake variables are weakly typed). ### 3.3 Loops * **Do This:** Use "foreach()" loops for iterating over lists. Use "while()" loops for more complex conditional iteration. """cmake set(MY_LIST "item1" "item2" "item3") foreach(ITEM IN LISTS MY_LIST) message(STATUS "Processing item: ${ITEM}") endforeach() set(COUNTER 0) while(COUNTER LESS 5) math(EXPR COUNTER "${COUNTER} + 1") message(STATUS "Counter: ${COUNTER}") endwhile() """ * **Don't Do This:** Use manual index manipulation within loops. Overuse or misuse "while()" loops. ### 3.4 Functions and Macros (Revisited) * **Do This:** Use functions for logical groupings of CMake commands that need to be reused with different arguments. Use macros for code fragments that need to be textually substituted into the calling scope (less common in modern CMake). """cmake # Function example function(add_source_files TARGET_NAME SOURCE_DIR) file(GLOB_RECURSE SOURCES "${SOURCE_DIR}/*.cpp") target_sources(${TARGET_NAME} PRIVATE ${SOURCES}) endfunction() # Call the function add_source_files(myExecutable src) # Macro example (use sparingly) macro(print_message MESSAGE) message(STATUS "${MESSAGE}") endmacro() # Call the macro print_message("Hello from a macro!") """ * **Don't Do This:** Use macros excessively (functions are generally preferred). Create functions or macros with side effects outside of their intended scope. ## 4. Project Structure A well-defined project structure makes CMake scripts easier to navigate and maintain. ### 4.1 Source Code Organization * **Do This:** Organize source code into logical directories (e.g., "src", "include", "test"). * **CMake Organization:** Mirror the source code structure in your CMake project. * Use "add_subdirectory()" to include CMakeLists.txt files in subdirectories. * Benefits: Enhanced encapsulation, separation of concerns, and improved modularity. """ project/ ├── CMakeLists.txt ├── src/ │ ├── CMakeLists.txt │ ├── main.cpp │ └── ... ├── include/ │ ├── mylib/ │ │ ├── header1.h │ │ └── header2.h │ └── ... └── test/ ├── CMakeLists.txt └── ... """ **"project/CMakeLists.txt":** """cmake cmake_minimum_required(VERSION 3.15) project(MyProject) add_subdirectory(src) add_subdirectory(test) """ **"project/src/CMakeLists.txt":** """cmake add_library(MyLibrary STATIC main.cpp) target_include_directories(MyLibrary PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/../include) """ * **Don't Do This:** Place all source files in a single directory. Create overly deep or complex directory structures. ### 4.2 Modules * **Do This:** Create reusable CMake modules for common tasks (e.g., finding dependencies, setting compiler flags). * Store modules in a dedicated directory (e.g., "cmake/Modules"). * Use the "CMAKE_MODULE_PATH" variable to tell CMake where to find your modules. **"cmake/Modules/FindMyDependency.cmake":** """cmake # Module to find MyDependency find_path( MYDEPENDENCY_INCLUDE_DIR NAMES mydependency.h PATHS /opt/mydependency/include ) if(MYDEPENDENCY_INCLUDE_DIR) set(MYDEPENDENCY_FOUND TRUE) else() set(MYDEPENDENCY_FOUND FALSE) endif() if(MYDEPENDENCY_FOUND) message(STATUS "Found MyDependency") else() message(WARNING "MyDependency not found") endif() mark_as_advanced(MYDEPENDENCY_INCLUDE_DIR) """ **"CMakeLists.txt":** """cmake list(APPEND CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/cmake/Modules") include(FindMyDependency) if(MYDEPENDENCY_FOUND) include_directories(${MYDEPENDENCY_INCLUDE_DIR}) # ... rest of the logic ... endif() """ * **Don't Do This:** Duplicate code across multiple CMakeLists.txt files. Fail to properly document custom modules. ### 4.3 Dependencies * **Do This:** Use "find_package()" to locate external dependencies. Use imported targets provided by "find_package()" to link against dependencies. Modern CMake strongly encourages the use of imported targets for dependency management. """cmake # Find the Boost library find_package(Boost REQUIRED COMPONENTS system filesystem) # Link against Boost (using imported target) if(Boost_FOUND) target_link_libraries(myExecutable PUBLIC Boost::system Boost::filesystem) #Use target based linking endif() """ * **Don't Do This:** Manually specify include directories and library paths for external dependencies. Use hardcoded paths. ## 5. Error Handling Robust error handling is essential for preventing unexpected build failures. ### 5.1 Checks and Assertions * **Do This:** Use "if()" statements to check for potential errors (e.g., missing files, invalid arguments). Use "message(FATAL_ERROR ...)" to abort the build if a critical error is encountered. Use "assert()" (if enabled, see below) for internal consistency checks. * **ASSERTIONS:** * Turning on Assertions: In your root CMakeLists.txt file, add the following line: """cmake set(CMAKE_BUILD_TYPE Debug) #Ensure Debug build type. """ * Example Usage of Assertions: """cmake # Check that a target exists if (NOT TARGET MyTarget) message(FATAL_ERROR "Target MyTarget does not exist.") endif() """ ### 5.2 Warnings * **Do This:** Use "message(WARNING ...)" to report non-critical issues to the user. """cmake if(NOT DEFINED MY_VARIABLE) message(WARNING "MY_VARIABLE is not defined. Using default value.") set(MY_VARIABLE "default") endif() """ * **Don't Do This:** Ignore potential errors or warnings. Suppress warnings without understanding their cause. ## 6. Build Configuration Proper build configuration ensures that the project can be built correctly in different environments. ### 6.1 Build Types * **Do This:** Use "CMAKE_BUILD_TYPE" to control the build type (e.g., Debug, Release). Set appropriate compiler flags based on the build type. * Users can set build type at configuration time "cmake -DCMAKE_BUILD_TYPE=Release .." """cmake if(CMAKE_BUILD_TYPE STREQUAL "Debug") target_compile_definitions(myExecutable PRIVATE DEBUG_MODE) # Enable debug mode endif() """ * **Don't Do This:** Hardcode build-type-specific logic in the CMakeLists.txt file. ### 6.2 Compiler Flags * **Do This:** Use "target_compile_options()" to set compiler flags for specific targets. Use "CMAKE_CXX_STANDARD" to set the C++ standard. * Prefer "target_compile_features" for more modern standards. """cmake set(CMAKE_CXX_STANDARD 17) # Set the C++ standard set(CMAKE_CXX_STANDARD_REQUIRED ON) # Require the C++ standard target_compile_options(myExecutable PRIVATE -Wall -Wextra) # Add warning flags """ * **Don't Do This:** Modify "CMAKE_CXX_FLAGS" directly (use "target_compile_options" instead). Use incompatible or deprecated compiler flags. ## 7. Platform Specific Code * **Do This:** Utilize CMake's built-in variables to determine the target platform and adjust build settings accordingly. """cmake if(WIN32) # Windows-specific settings message(STATUS "Configuring for Windows") target_compile_definitions(MyTarget PRIVATE WIN32_LEAN_AND_MEAN) # Define a macro for Windows builds elseif(UNIX) # Unix-like settings message(STATUS "Configuring for Unix") endif() """ * **Don't Do This:** Rely on environment variables or external tools to detect the platform. This reduces portability. ## 8. Security Considerations * **Do This:** Avoid executing arbitrary commands or scripts during the CMake configuration process. Be cautious when using "execute_process", and always validate inputs. Use secure coding practices in your C++ code to prevent vulnerabilities. * **Don't Do This:** Embed sensitive information (e.g., passwords, API keys) directly in CMakeLists.txt files. ## 9. Continuous Integration * **Do This:** Integrate CMake into your continuous integration (CI) system. Use a consistent build environment across all CI builds. Run unit tests as part of the CI process. * **Don't Do This:** Rely on manual builds for testing. Use different build environments for development and CI. By adhering to these code style and conventions, we can ensure that our CMake scripts are readable, maintainable, and robust. This will improve collaboration, reduce errors, and ultimately lead to higher-quality software.