# 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
$
$
)
# 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=$<$: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.
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.
# Core Architecture Standards for CMake This document outlines the core architecture standards for CMake projects. Following these standards ensures maintainability, scalability, and readability, leading to more robust and efficient builds. These standards are designed for the latest CMake versions, leveraging modern patterns and best practices. ## 1. Project Structure and Organization A well-defined project structure is crucial for navigating and maintaining CMake projects. ### 1.1. Standard Directory Layout **Do This:** Adopt a consistent directory structure across projects. This allows developers to quickly understand the project layout, regardless of its complexity. This example follows a source/include separation, which is a common and preferred approach. """ my_project/ ├── CMakeLists.txt # Top-level CMake file ├── include/ # Public header files │ └── my_project/ │ └── my_class.h # Public header for MyClass ├── src/ # Source files │ ├── CMakeLists.txt # CMake file for source directory │ └── my_class.cpp # Implementation of MyClass ├── test/ # Test files │ ├── CMakeLists.txt # CMake file for testing │ └── my_class_test.cpp # Tests for MyClass └── cmake/ # Custom CMake modules └── FindMyLib.cmake # Optional: Find module for MyLib """ **Don't Do This:** Scatter source files and headers arbitrarily throughout the project, mixing unrelated files and folders. **Why:** A standard structure simplifies navigation, maintenance, and onboarding. Separating public headers from implementation makes dependency management easier. The "cmake/" directory encapsulates custom logic, promoting reusability. ### 1.2. Top-Level CMakeLists.txt **Do This:** Keep the top-level "CMakeLists.txt" simple and focused on project-wide settings. """cmake cmake_minimum_required(VERSION 3.20) # or later project(MyProject VERSION 1.0.0 LANGUAGES CXX) # Global compiler flags (e.g., C++ standard) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) # Include directories include_directories(include) # Subdirectory definitions add_subdirectory(src) add_subdirectory(test) # Install rules (e.g., libraries and headers) install( TARGETS MyProjectExecutable # or MyProjectLibrary DESTINATION bin ) install( DIRECTORY include/my_project DESTINATION include ) """ **Don't Do This:** Implement complex logic, target definitions, or source file listings directly in the top-level file. **Why:** The top-level file should provide a clear overview. Subdirectories encapsulate complexity. The "install" commands defines where the build artifacts are copied. ### 1.3. Subdirectory CMakeLists.txt **Do This:** Use "add_library" and "add_executable" within each subdirectory to define targets and manage source files specific to that directory. """cmake # src/CMakeLists.txt add_library(MyProjectLibrary STATIC my_class.cpp) target_include_directories(MyProjectLibrary PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/../include") # test/CMakeLists.txt enable_testing() add_executable(MyProjectTests my_class_test.cpp) target_link_libraries(MyProjectTests MyProjectLibrary) include(CTest) """ """cmake # An example test: test/my_class_test.cpp #include <gtest/gtest.h> // Or any testing framework #include "my_project/my_class.h" TEST(MyClassTest, BasicTest) { MyClass obj; ASSERT_EQ(obj.getValue(), 0); } int main(int argc, char **argv) { ::testing::InitGoogleTest(&argc, argv); return RUN_ALL_TESTS(); } """ **Don't Do This:** Define global variables or targets within a subdirectory that affect other parts of the project without scoping them. **Why:** Subdirectories provide encapsulation. "target_include_directories" manages include paths for specific targets. Linking to the library in the test ensures testability. ### 1.4. Custom CMake Modules **Do This:** Create custom modules for reusable build logic in the "cmake/" directory. For example, to find a library: """cmake # cmake/FindMyLib.cmake find_path(MYLIB_INCLUDE_DIR mylib.h PATHS /opt/mylib/include) find_library(MYLIB_LIBRARY mylib PATHS /opt/mylib/lib) include(FindPackageHandleStandardArgs) find_package_handle_standard_args(MyLib DEFAULT_MSG MYLIB_LIBRARY MYLIB_INCLUDE_DIR) if(MYLIB_FOUND) set(MYLIB_INCLUDE_DIRS ${MYLIB_INCLUDE_DIR}) set(MYLIB_LIBRARIES ${MYLIB_LIBRARY}) endif() mark_as_advanced(MYLIB_INCLUDE_DIR MYLIB_LIBRARY) """ To use the module: """cmake # Your main CMakeLists.txt list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake") find_package(MyLib REQUIRED) if (MYLIB_FOUND) include_directories(${MYLIB_INCLUDE_DIRS}) target_link_libraries(MyExecutable ${MYLIB_LIBRARIES}) else() # optional section. message(FATAL_ERROR "MyLib not found!") endif() """ **Don't Do This:** Hardcode library paths directly within the "CMakeLists.txt" or duplicate the same find logic across multiple projects. **Why:** Modules encapsulate reusable build logic. "find_package" simplifies external dependency handling. ## 2. Modern CMake Features and Best Practices Leveraging modern features available in recent CMake versions greatly improves project maintainability. ### 2.1. Target-Based Management **Do This:** Always define targets using "add_library" or "add_executable" and manage properties via "target_*" commands. """cmake add_library(MyLibrary STATIC my_class.cpp) target_include_directories(MyLibrary PUBLIC include) target_compile_definitions(MyLibrary PUBLIC -DDEBUG_MODE) # Compile definitions target_compile_features(MyLibrary PUBLIC cxx_std_17) # CXX Standard feature """ **Don't Do This:** Set global properties using "set" that affect the entire build or modify include paths globally with "include_directories". **Why:** Target-based management provides fine-grained control and avoids unintended side effects. It promotes modularity and encapsulation. Modern CMake heavily emphasizes this approach. "target_compile_features" makes setting the C++ standard more reliable, as the compiler checks that the requested standard is at least supported. ### 2.2. Interface Libraries **Do This:** Use "INTERFACE" libraries to represent header-only libraries or shared interfaces. """cmake add_library(MyInterface INTERFACE) target_include_directories(MyInterface INTERFACE include) target_compile_definitions(MyInterface INTERFACE -DAPI_EXPORTS) # Use MyInterface in another library or executable add_library(MyImplementation SHARED my_implementation.cpp) target_link_libraries(MyImplementation MyInterface) """ **Don't Do This:** Create a regular library target for header-only code. **Why:** Interface libraries correctly propagate include directories, compile definitions, and other properties to their dependents without generating any actual build output. ### 2.3. Generator Expressions **Do This:** Use [generator expressions](https://cmake.org/cmake/help/latest/manual/cmake-generator-expressions.7.html) for conditional settings based on the build configuration (Debug/Release), target properties, or platform. """cmake add_executable(MyApp main.cpp) target_compile_definitions(MyApp PRIVATE $<$<CONFIG:Debug>:DEBUG_MODE> $<$<CONFIG:Release>:NDEBUG> ) target_link_libraries(MyApp $<$<PLATFORM_ID:Windows>:Advapi32> # Link against Advapi32 on Windows only ) """ **Don't Do This:** Use complex "if" statements or scripting logic within "CMakeLists.txt" to handle configuration-specific settings. **Why:** Generator expressions provide a clean and declarative way to express configuration-dependent properties. They improve readability and maintainability compared to complex conditional logic. ### 2.4. Dependency Management with Find Modules or Package Configs **Do This:** Prefer "find_package" to locate external dependencies. Use "find_package(PackageName REQUIRED)" to enforce that a dependency is found. This is more maintainable than manually specifying include directories and library paths. If a "FindPackageName.cmake" module isn't available, consider creating one or using a package config file (if the dependency provides one). Use tools like "CPM.cmake" or "FetchContent" to manage dependencies that may not be available on the system. """cmake # Using find_package find_package(ZLIB REQUIRED) # standard find module if(ZLIB_FOUND) include_directories(${ZLIB_INCLUDE_DIRS}) target_link_libraries(MyExecutable ${ZLIB_LIBRARIES}) endif() # Using FetchContent (requires CMake 3.11 or later) to automatically download dependencies include(FetchContent) FetchContent_Declare( googletest GIT_REPOSITORY https://github.com/google/googletest.git GIT_TAG release-1.11.0 ) FetchContent_MakeAvailable(googletest) include_directories(${googletest_SOURCE_DIR}/googletest/include) # Adjust as needed target_link_libraries(MyExecutable gtest gtest_main) # link to gtest """ **Don't Do This:** Manually specify include directories and library paths for external dependencies, or copy dependency code directly into your project when a package is available. **Why:** "find_package" provides a standard mechanism for locating and using external dependencies. "FetchContent" simplifies the process of integrating dependencies hosted in repositories. It improves portability and reduces code duplication. These practices also prevent conflicts caused by copies of library code, particularly in cases of transitive dependencies. ### 2.5. Using "CMAKE_AUTOUIC", "CMAKE_AUTOMOC", and "CMAKE_AUTORCC" **Do This**: Enable automatic processing of Qt files if using Qt. """cmake set(CMAKE_AUTOUIC ON) set(CMAKE_AUTOMOC ON) set(CMAKE_AUTORCC ON) """ **Don't Do This**: Manually run "uic", "moc", and "rcc". **Why**: CMake will automatically find and process Qt-related files, simplifying the build process. ## 3. CMake Code Style and Conventions Adhering to a consistent code style significantly improves the readability and maintainability of CMake code. ### 3.1. Formatting **Do This:** Use consistent indentation and spacing. Typically, two or four spaces are used for indentation. Place each command on a new line for readability. Use uppercase for CMake keywords and lowercase for project-specific variables. """cmake # Good formatting: set(MY_VARIABLE "some_value") add_library(MyLibrary STATIC my_class.cpp) # Bad formatting: set(MY_VARIABLE "some_value") #hard to read add_library(MyLibrary STATIC my_class.cpp) # awkward """ **Don't Do This:** Use inconsistent indentation, excessively long lines, or cram multiple commands onto a single line. **Why:** Consistent formatting improves readability and reduces the likelihood of errors. ### 3.2. Comments **Do This:** Provide detailed comments to explain complex logic, non-obvious choices, and the purpose of each section. If you find that you cannot comment, then your code is too complex and needs to be refactored. """cmake # Find the ZLIB library find_package(ZLIB REQUIRED) if(ZLIB_FOUND) # Add the ZLIB include directory to the project include_directories(${ZLIB_INCLUDE_DIRS}) # Link the ZLIB library to the MyExecutable target target_link_libraries(MyExecutable ${ZLIB_LIBRARIES}) endif() """ **Don't Do This:** Write self-evident comments or omit important information. **Why:** Clear comments improve understanding and make it easier to maintain the code. ### 3.3. Variable Naming **Do This:** Use descriptive variable names. Prefix variables with the project or module name (e.g., "MYPROJECT_LIB_VERSION"). Use "UPPER_SNAKE_CASE" for constants and "lower_snake_case" for variables that change. """cmake set(MY_PROJECT_VERSION "1.2.3") set(my_source_files "main.cpp" "my_class.cpp") """ **Don't Do This:** Use short, ambiguous names or inconsistent naming schemes. **Why:** Clear variable names improve readability and reduce the risk of naming conflicts. ### 3.4. Scope Management **Do This:** Use "set(VAR "value" PARENT_SCOPE)" to pass variables to the parent scope. Use "unset(VAR)" to remove variables when they are no longer needed. """cmake # In a subdirectory CMakeLists.txt set(MY_SUBDIRECTORY_VARIABLE "some_value" PARENT_SCOPE) """ **Don't Do This:** Rely on global variables or modify variables in unexpected scopes. **Why:** Proper scope management prevents unintended side effects. ## 4. Performance and Optimization CMakeLists.txt files should not be a source for performance regressions in your build processes. ### 4.1. Minimizing Unnecessary Operations **Do This:** Avoid unnecessary file system operations (e.g., globbing) whenever possible. Use explicit source lists. """cmake # Preferred: Explicit source list set(MY_SOURCES main.cpp my_class.cpp ) add_executable(MyApp ${MY_SOURCES}) # Anti-pattern: Globbing (can be slow, especially on large projects) # file(GLOB MY_SOURCES "src/*.cpp") # add_executable(MyApp ${MY_SOURCES}) """ **Don't Do This:** Heavily rely on file globbing, especially in large projects. **Why:** File globbing can be slow, especially on large projects, as CMake needs to re-scan the file system whenever "CMakeLists.txt" is processed. ### 4.2. Incremental Builds **Do This:** Ensure that the build system supports incremental builds. Avoid actions that invalidate the entire build cache unnecessarily. Using target-based compiling and linking ensures that only the necessary files are rebuilt when changes occur. **Don't Do This:** Change critical build variables or options that will force CMake to reconfigure the entire project on every build. **Why:** Supporting incremental builds significantly speeds up the development cycle by only rebuilding the components that have changed. ### 4.3. Using Precompiled Headers (PCH) **Do This:** Use precompiled headers to reduce the compilation time, especially if you are consistently including the same headers across multiple source files. """cmake # Example usage in CMakeLists.txt set_property(SOURCE pch.h PROPERTY CXX_STANDARD_REQUIRED ON) # PCH requires the correct standard add_library(my_pch OBJECT pch.h pch.cpp) # Create object library for the PCH set_target_properties(my_pch PROPERTIES COMPILE_FLAGS "-include pch.h") target_link_libraries(MyExecutable PRIVATE my_pch) # link to ensure compilation target_precompile_headers(MyExecutable PUBLIC pch.h) # modern approach available with CMake 3.16+ and newer compilers. """ **Don't Do This:** Neglect using precompiled headers when dealing with large projects, or unnecessarily include headers that are not commonly used. **Why:** Decreases compile times. The target_precompile_headers is the modern way to handle this within CMake (3.16+). ## 5. Security Considerations Security should be a primary consideration in all software projects, including CMake configuration. ### 5.1. Preventing Command Injection **Do This:** Avoid constructing shell commands directly from user-provided input. Escaping user input is generally not enough. If you must execute external commands, prefer using the "execute_process" command with its arguments passed as a list rather than a single string to avoid shell injection. """cmake # Safer approach: execute_process( COMMAND ${CMAKE_COMMAND} -E echo ${user_input} OUTPUT_VARIABLE output ERROR_VARIABLE error ) # Very unsafe. Do not use! # execute_process(COMMAND ${CMAKE_COMMAND} -E "echo ${user_input}") """ **Don't Do This:** Construct shell commands directly from user-provided input. **Why:** Prevents command injection vulnerabilities. ### 5.2. Validating Input **Do This:** Validate all user-provided input, especially when used in file paths or commands. Use "string(REGEX MATCH)" to validate input against expected patterns. Use "file(TO_CMAKE_PATH)" to ensure that paths are properly formatted before use. """cmake string(REGEX MATCH "^[a-zA-Z0-9_]+$" valid_input "${user_input}") if(NOT valid_input) message(FATAL_ERROR "Invalid input: ${user_input}") endif() """ **Don't Do This:** Trust user input blindly. **Why:** Prevents path traversal and other vulnerabilities. ### 5.3. Secure Defaults **Do This:** Set secure defaults for compile and link flags. Enable compiler warnings and treat warnings as errors. Use AddressSanitizer and UndefinedBehaviorSanitizer for testing. If available, enable position independent executables (PIE). """cmake # Enable common compiler warnings if(CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang") target_compile_options(MyTarget PRIVATE -Wall -Wextra -Werror ) endif() # Enable sanitizers for Debug builds if(CMAKE_BUILD_TYPE MATCHES "Debug") target_compile_options(MyTarget PRIVATE -fsanitize=address,undefined) target_link_options(MyTarget PRIVATE -fsanitize=address,undefined) endif() set(CMAKE_POSITION_INDEPENDENT_CODE ON) # Enables PIE """ **Don't Do This:** Use insecure defaults or disable security features. **Why:** Reduces the risk of security vulnerabilities. By adhering to these core architecture standards, CMake projects will be better organized, more maintainable, and more secure. Modern CMake provides powerful features, and their correct usage is key to building robust and scalable build systems.
# Component Design Standards for CMake This document outlines coding standards specifically for component design in CMake. The focus is on creating reusable, maintainable, and scalable CMake code using modern practices, targeting the latest CMake version. ## 1. Introduction to CMake Component Design Component design in CMake involves organizing code into modular, independent units that can be reused across different projects or within the same project. This promotes code reuse, reduces redundancy, improves maintainability, and simplifies collaboration. * **Goals:** Promote modularity, reusability, maintainability, and collaboration. * **Focus:** Defining, organizing, and using components in CMake projects. ## 2. Architectural Considerations ### 2.1 Define Component Boundaries Clearly * **Do This:** Define clear interfaces and responsibilities for each component. A component should have a well-defined purpose and should encapsulate a specific set of functionality. * **Don't Do This:** Create components with overlapping responsibilities or poorly defined interfaces. Avoid "god modules" that try to do everything. **Why:** Clear boundaries simplify understanding, testing, and maintenance. Well-defined interfaces reduce coupling and make components easier to reuse. **Example:** """cmake # my_component/CMakeLists.txt cmake_minimum_required(VERSION 3.20) # Example version declaration project(MyComponent) # Define the interface target add_library(MyComponent INTERFACE) # Include directories for the component's public headers target_include_directories(MyComponent INTERFACE $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include> $<INSTALL_INTERFACE:include> ) # Define properties of the INTERFACE target - these will be inherited # by any target that *links* to this one. Consider IMPORTED_CONFIGURATIONS # if different exported configurations need different values. set_property(TARGET MyComponent PROPERTY INTERFACE_COMPILE_DEFINITIONS "MY_COMPONENT_ENABLED") # Install the component's headers (public interface!) install(DIRECTORY include/ DESTINATION include COMPONENT MyComponent) # Use a component name # Export the target for use in other projects (if you intend for this to be consumed by other projects) install(TARGETS MyComponent EXPORT MyComponentTargets NAMESPACE MyNamespace:: DESTINATION lib/cmake/MyComponent COMPONENT MyComponent) # Create export set for packaging export(TARGETS MyComponent FILE "${CMAKE_CURRENT_BINARY_DIR}/MyComponentTargets.cmake") include(CMakePackageConfigHelpers) write_basic_package_version_file( "${CMAKE_CURRENT_BINARY_DIR}/MyComponentConfigVersion.cmake" VERSION "${PROJECT_VERSION}" COMPATIBILITY SameMajorVersion ) # Generate the config file configure_package_config_file( "${CMAKE_CURRENT_SOURCE_DIR}/MyComponentConfig.cmake.in" # Make sure this file exists! "${CMAKE_CURRENT_BINARY_DIR}/MyComponentConfig.cmake" INSTALL_DESTINATION "lib/cmake/MyComponent" ) install(FILES "${CMAKE_CURRENT_BINARY_DIR}/MyComponentConfig.cmake" "${CMAKE_CURRENT_BINARY_DIR}/MyComponentConfigVersion.cmake" DESTINATION lib/cmake/MyComponent COMPONENT MyComponent ) """ """cmake # my_component/include/my_component/my_component.h #ifndef MY_COMPONENT_MY_COMPONENT_H #define MY_COMPONENT_MY_COMPONENT_H #ifdef MY_COMPONENT_ENABLED #define MY_COMPONENT_API __declspec(dllexport) // Or equivalent for other platforms #else #define MY_COMPONENT_API __declspec(dllimport) #endif namespace MyNamespace { class MY_COMPONENT_API MyComponentClass { public: MyComponentClass(); int doSomething(); }; } // namespace MyNamespace #endif // MY_COMPONENT_MY_COMPONENT_H """ """cmake # my_component/MyComponentConfig.cmake.in # Defines the interface to the MyComponent library. include(${CMAKE_CURRENT_LIST_DIR}/MyComponentTargets.cmake) # Helper function for finding all the dependencies. include(FindPackageHandleStandardArgs) find_package_handle_standard_args(MyComponent REQUIRED_VARS MyComponent_FOUND VERSION_VAR MyComponent_VERSION ) mark_as_advanced(MyComponent_FOUND MyComponent_VERSION) """ **Anti-Pattern:** Creating a single, monolithic CMakeLists.txt file that manages the entire project. ### 2.2 Prioritize Interface Targets * **Do This:** Use "INTERFACE" libraries to define the public API of a component. Interface libraries act as a contract, defining the include directories, compile definitions, and link dependencies required to use the component. Crucially, they do not contribute any code themselves, they are pure metadata. * **Don't Do This:** Directly expose implementation details or internal headers through global CMake variables. Avoid directly manipulating compiler flags or linker settings outside the component's CMakeLists.txt. **Why:** Interface libraries provide a clean, explicit way to define the component's public API. They improve encapsulation and reduce the risk of conflicts between components. **Example:** """cmake # my_component/CMakeLists.txt (as shown above in 2.1 example) add_library(MyComponent INTERFACE) target_include_directories(MyComponent INTERFACE $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include> $<INSTALL_INTERFACE:include> # Install-time path to headers ) target_compile_definitions(MyComponent INTERFACE "MY_COMPONENT_ENABLED" # Defined when using the component ) # Example of requiring a dependency. Better to call find_package() in the CONSUMING project # target_link_libraries(MyComponent INTERFACE AnotherComponent) """ **Anti-Pattern:** Making assumptions about the consuming project's environment or compiler settings. ### 2.3 Use Namespaces to Prevent Collisions * **Do This:** Encapsulate components within namespaces to avoid naming conflicts. Use a consistent naming scheme for targets, variables, and functions. * **Don't Do This:** Use global, unqualified names that may clash with other components or libraries. **Why:** Namespaces provide a clear separation between components and prevent accidental naming conflicts. **Example:** """cmake # my_component/CMakeLists.txt add_library(MyNamespace::MyComponent INTERFACE) # Namespaced target set_target_properties(MyNamespace::MyComponent PROPERTIES EXPORT_NAME MyComponent) # For older versions """ In C++: """c++ // my_component/include/my_component/my_component.h namespace MyNamespace { class MyComponentClass { // ... }; } // namespace MyNamespace """ **Anti-Pattern:** Using short, generic names like "LIB" or "UTIL" for targets or variables. ### 2.4 Package Components for Distribution * **Do This:** Use "install()" and "export()" commands to package components for distribution. Create a CMake package configuration file (e.g., "MyComponentConfig.cmake") that defines how to use the component in other projects. * **Don't Do This:** Manually copy header files or libraries. Rely on consuming projects to find dependencies or configure compiler settings. **Why:** Packaging ensures that components can be easily integrated into other projects without requiring manual configuration or dependency management. **Example:** """cmake # my_component/CMakeLists.txt (as shown above in 2.1 example) install(TARGETS MyComponent EXPORT MyComponentTargets NAMESPACE MyNamespace:: DESTINATION lib/cmake/MyComponent COMPONENT MyComponent) export(TARGETS MyComponent FILE "${CMAKE_CURRENT_BINARY_DIR}/MyComponentTargets.cmake") """ **Anti-Pattern:** Requiring users to manually set CMake variables or modify their build scripts to use a component. ## 3. Implementation Details ### 3.1 Use Target Properties * **Do This:** Use "target_include_directories", "target_compile_definitions", and "target_link_libraries" to configure target properties. Avoid using global variables or directly setting compiler flags. Use generator expressions for conditional configuration. * **Don't Do This:** Use "include_directories", "add_definitions", or "link_directories" commands, as these affect the entire project rather than specific targets. **Why:** Target properties provide a localized and explicit way to configure targets. This reduces the risk of unintended side effects and makes it easier to understand the dependencies of each target. **Example:** """cmake # my_component/CMakeLists.txt add_library(MyComponent STATIC src/my_component.cpp) target_include_directories(MyComponent PUBLIC $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include> $<INSTALL_INTERFACE:include> ) target_compile_definitions(MyComponent PUBLIC "MY_COMPONENT_ENABLED" ) target_link_libraries(MyComponent PUBLIC AnotherComponent # Assuming you have another component defined ) """ **Anti-Pattern:** Adding include directories or compile definitions to the global scope. ### 3.2 Leverage Generator Expressions * **Do This:** Use generator expressions ("$<...>", "$<$CONFIG:...>") to conditionally configure targets based on the build environment. * **Don't Do This:** Rely on manual checks or conditional logic within CMakeLists.txt files. **Why:** Generator expressions allow you to define build-time conditions without modifying the CMakeLists.txt file. This improves flexibility and simplifies cross-platform builds. **Example:** """cmake # my_component/CMakeLists.txt target_compile_definitions(MyComponent PRIVATE "$<$<CONFIG:Debug>:DEBUG_MODE>" # Only define DEBUG_MODE in Debug configuration ) target_link_libraries(MyComponent PRIVATE "$<$<PLATFORM_ID:Windows>:some_windows_lib>" ) """ **Anti-Pattern:** Using "if()" statements with "CMAKE_BUILD_TYPE" or other global variables to configure targets. ### 3.3 Encapsulate Implementation Details * **Do This:** Hide implementation details within the component. Expose only the necessary headers and libraries through the public API. Use the "PRIVATE" and "INTERFACE" keywords to control visibility. * **Don't Do This:** Expose internal headers or implementation details through the public API. **Why:** Encapsulation protects the component from changes to its implementation and reduces the risk of breaking compatibility with other components. **Example:** """cmake # my_component/CMakeLists.txt add_library(MyComponent STATIC src/my_component.cpp src/internal_implementation.cpp) target_include_directories(MyComponent PUBLIC $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include> $<INSTALL_INTERFACE:include> PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src/internal # Don't expose this! ) """ In C++: """c++ // my_component/src/internal_implementation.cpp #include "internal_implementation.h" // Not exposed in public headers """ **Anti-Pattern:** Including internal headers in the component's public API. ### 3.4 Manage Dependencies Explicitly * **Do This:** Use "find_package()" to locate external dependencies. Use "target_link_libraries()" to link against the required libraries. Specify minimum version requirements. Ensure dependencies are installed as part of continuous integration. * **Don't Do This:** Assume that dependencies are already installed or located in a specific directory. Rely on global variables or manual configuration to find dependencies. **Why:** Explicit dependency management ensures that the component can be built and used in different environments. It avoids version conflicts and simplifies dependency resolution. **Example:** """cmake # my_component/CMakeLists.txt find_package(Boost 1.70 REQUIRED COMPONENTS system filesystem) # Specify minimum version if (Boost_FOUND) target_include_directories(MyComponent PUBLIC ${Boost_INCLUDE_DIRS} ) target_link_libraries(MyComponent PUBLIC Boost::system # Modern target-based linking ) else() message(FATAL_ERROR "Boost library not found.") endif() """ **Anti-Pattern:** Directly linking to system libraries without using "find_package()". For example, hardcoding "/usr/lib/libcurl.so". ## 4. Advanced Techniques ### 4.1 Custom Commands and Targets * **Do This:** Use custom commands and custom targets to automate build-time tasks. Wrap complex logic into functions for reusability. * **Don't Do This:** Perform manual steps or rely on external scripts to execute build-time tasks. **Why:** Custom commands and targets enable you to integrate external tools and processes into the build system. They improve automation and reduce the risk of human error. **Example:** """cmake # my_component/CMakeLists.txt add_custom_command( OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/generated_file.txt COMMAND ${CMAKE_COMMAND} -E echo "Hello, world!" > ${CMAKE_CURRENT_BINARY_DIR}/generated_file.txt DEPENDS src/input_file.txt # Re-run if input file changes ) add_custom_target(GenerateFile DEPENDS ${CMAKE_CURRENT_BINARY_DIR}/generated_file.txt ) add_dependencies(MyComponent GenerateFile) # Ensure file is generated before building component """ **Anti-Pattern:** Running external scripts directly from the command line or relying on manual configuration steps. ### 4.2 Abstract Factories and Plugin Architectures (Conceptual, not CMake syntax) Modern C++ component design often leverages patterns like abstract factories to choose implementations at runtime. These components can be dynamically loaded as plugins. While CMake does not directly implement these patterns, it provides the build infrastructure. * "add_library(... MODULE)" creates a dynamically loadable module. * Configure include paths to abstract interfaces to allow code to be compiled independently from specific plug-ins. * Use "install()" commands to copy modules to a plugin directory with a well-defined structure. * Write CMake code to discover plugins at runtime (using "glob()" or similar). ### 4.3 Testing and Continuous Integration * **Do This:** Integrate unit tests and integration tests into the build process. Use a testing framework like Catch2 or Google Test. Set up continuous integration to automatically build and test the component on different platforms. Use code coverage tools to measure the test coverage. **Why:** Testing ensures that the component is working correctly and that changes do not introduce regressions. Continuous integration automates the build and testing process and provides early feedback on potential problems. **Example:** """cmake # my_component/CMakeLists.txt enable_testing() add_executable(MyComponentTests test/my_component_tests.cpp) target_link_libraries(MyComponentTests MyComponent) include(GoogleTest) gtest_discover_tests(MyComponentTests) """ **Anti-Pattern:** Skipping unit tests or relying on manual testing to verify the correctness of the component. Failing to regularly build the component with a continuous build system. ## 5. Conclusion By adhering to these component design standards, you can create reusable, maintainable, and scalable CMake code. This will improve collaboration, reduce redundancy, and simplify the development process. Remember to leverage the latest CMake features and best practices to ensure that your code is modern and efficient. This document will be continually updated as new CMake features and best practices emerge. Consult official CMake documentation and release notes regularly.
# 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 $<$<CONFIG:Debug>:DEBUG_MODE>) # Only define DEBUG_MODE in Debug builds target_link_libraries(myexe PRIVATE $<$<PLATFORM_ID:Windows>: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.
# API Integration Standards for CMake This document outlines coding standards and best practices for integrating external APIs and backend services within CMake projects. It focuses on modern CMake approaches (version 3.15 and later) to ensure maintainability, performance, and security. ## 1. Architectural Considerations for API Integration ### 1.1. Principle of Separation of Concerns **Standard:** Isolate API integration logic from core build logic. CMake should orchestrate the build process, not directly handle complex API communication. **Do This:** Create separate CMake modules or scripts specifically for handling API interactions. **Don't Do This:** Embed API calls directly within "CMakeLists.txt" files that define target compilation or linking. **Why:** Increases modularity, simplifies testing, and reduces the risk of build failures due to API downtime or changes. **Example:** """cmake # CMakeLists.txt include(cmake/API_Integration) add_executable(my_app main.cpp) target_link_libraries(my_app ${API_LIBRARIES}) # Define API_LIBRARIES in API_Integration.cmake # cmake/API_Integration.cmake include(ExternalProject) ExternalProject_Add( my_api_dependency GIT_REPOSITORY "https://github.com/example/my-api-library.git" GIT_TAG "v1.2.3" CONFIGURE_COMMAND "" # Prevents configure step, assuming library provides a header-only interface BUILD_COMMAND "" # Prevents build step, assuming library is prebuilt INSTALL_COMMAND "" # Prevents install step, we find package or include directory ) set(API_INCLUDE_DIR "${CMAKE_BINARY_DIR}/my_api_dependency/src/my_api_dependency") include_directories(${API_INCLUDE_DIR}) # Potentially use find_package for cases where the API can be satisfied through system packages find_package(MyAPIPackage REQUIRED) set(API_LIBRARIES MyAPIPackage::MyAPILibrary) add_library(api_interface INTERFACE) target_include_directories(api_interface INTERFACE ${API_INCLUDE_DIR}) # Can add more fine grained interface dependencies if necessary if(TARGET MyAPIPackage::MyAPILibrary) target_link_libraries(api_interface INTERFACE MyAPIPackage::MyAPILibrary) endif() # Set global property for use. This is an alternative to target_link_libraries and target_include_directories when appropriate. set_property(GLOBAL PROPERTY API_LIBRARIES ${API_LIBRARIES}) """ ### 1.2. Declarative Dependency Management **Standard:** Describe API dependencies declaratively. Avoid imperative procedures for downloading or installing them within core build scripts. **Do This:** Use "find_package()", "FetchContent", or "ExternalProject_Add()" with appropriate configurations for managing API dependencies. **Don't Do This:** Manually download or build API libraries within "CMakeLists.txt" files using shell commands. **Why:** Facilitates reproducible builds, simplifies dependency updates, and improves portability across different platforms. **Example (FetchContent):** """cmake include(FetchContent) FetchContent_Declare( boost GIT_REPOSITORY "https://github.com/boostorg/boost.git" GIT_TAG "boost-1.84.0" # Use a specific tag for reproducibility ) FetchContent_MakeAvailable(boost) add_executable(my_app main.cpp) target_link_libraries(my_app Boost::system Boost::filesystem) # Use imported targets target_include_directories(my_app PUBLIC ${Boost_INCLUDE_DIRS}) """ **Example (find_package):** """cmake find_package(CURL REQUIRED) if (CURL_FOUND) add_executable(my_app main.cpp) target_link_libraries(my_app ${CURL_LIBRARIES}) target_include_directories(my_app PUBLIC ${CURL_INCLUDE_DIRS}) else() message(FATAL_ERROR "CURL library not found. Please install it.") endif() """ ### 1.3. Abstraction Layers **Standard:** Create abstraction layers in your codebase to decouple your application logic from the specifics of the external API. **Do This:** Define consistent interfaces or abstract classes in your C++ code that represent API functionality. Implement these interfaces using API-specific classes. **Don't Do This:** Directly use API classes and functions throughout your codebase. **Why:** Simplifies testing, enables easier switching to alternative APIs in the future, and improves overall code maintainability. **Example:** C++ code """cpp // api_wrapper.h class APIWrapper { public: virtual ~APIWrapper() = default; virtual std::string fetchData(const std::string& query) = 0; }; // concrete_api_wrapper.h (uses a specific API, e.g., CURL) #include "api_wrapper.h" #include <curl/curl.h> class ConcreteAPIWrapper : public APIWrapper { public: std::string fetchData(const std::string& query) override { // Implement API call using CURL CURL *curl; CURLcode res; std::string readBuffer; curl = curl_easy_init(); if(curl) { curl_easy_setopt(curl, CURLOPT_URL, query.c_str()); curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, WriteCallback); curl_easy_setopt(curl, CURLOPT_WRITEDATA, &readBuffer); res = curl_easy_perform(curl); curl_easy_cleanup(curl); if (res != CURLE_OK) { throw std::runtime_error(curl_easy_strerror(res)); } return readBuffer; } throw std::runtime_error("curl init failed"); } private: static size_t WriteCallback(void *contents, size_t size, size_t nmemb, void *userp) { ((std::string*)userp)->append((char*)contents, size * nmemb); return size * nmemb; } }; // main.cpp #include "api_wrapper.h" #include "concrete_api_wrapper.h" #include <iostream> int main() { APIWrapper* api = new ConcreteAPIWrapper(); try { std::string data = api->fetchData("https://example.com/api/data"); std::cout << "Data from API: " << data << std::endl; } catch(const std::runtime_error& e) { std::cerr << "API Error: " << e.what() << std::endl; } delete api; return 0; } """ ## 2. API Dependency Management in CMake ### 2.1. Using "find_package()" **Standard:** Prefer "find_package()" where available for known libraries or packages. Rely on official Find Modules or create your own. **Do This:** Use "find_package(MyLibrary REQUIRED)" to locate the library and its dependencies. Use imported targets (e.g., "MyLibrary::MyTarget") when provided. **Don't Do This:** Hardcode paths to library files and headers. **Why:** Integrates with system package managers, respects install prefixes, and provides a standardized way to locate dependencies. **Example:** """cmake find_package(Boost REQUIRED COMPONENTS system filesystem) if (Boost_FOUND) add_executable(my_app main.cpp) target_link_libraries(my_app Boost::system Boost::filesystem) target_include_directories(my_app PRIVATE ${Boost_INCLUDE_DIRS}) else() message(FATAL_ERROR "Boost library not found. Please install it.") endif() """ ### 2.2. Using "FetchContent" **Standard:** Use "FetchContent" for header-only libraries, libraries directly managed in source, or small dependencies not readily available via "find_package()". **Do This:** Define the repository URL and tag or branch in "FetchContent_Declare()". Use "FetchContent_MakeAvailable()" to integrate the content into the build. Handle potential errors. **Don't Do This:** Re-download the dependency on every build. **Why:** Provides a straightforward way to integrate external code directly into your project without external configuration. Handles dependency resolution within CMake. **Example:** """cmake include(FetchContent) FetchContent_Declare( nlohmann_json GIT_REPOSITORY "https://github.com/nlohmann/json.git" GIT_TAG "v3.11.3" # Use a specific tag ) FetchContent_MakeAvailable(nlohmann_json) add_executable(my_app main.cpp) target_include_directories(my_app PRIVATE ${nlohmann_json_SOURCE_DIR}/include) """ ### 2.3. Using "ExternalProject_Add()" **Standard:** Use "ExternalProject_Add()" for complex dependencies that require build steps or have non-standard installation procedures. Avoid for header-only libraries. **Do This:** Specify "CONFIGURE_COMMAND", "BUILD_COMMAND", and "INSTALL_COMMAND" as necessary. Ensure appropriate error handling and logging. Consider using "CMAKE_ARGS", "BUILD_BYPRODUCTS", and "STEP_TARGETS". **Don't Do This:** Embed complex build logic directly into the "CMakeLists.txt" file. **Why:** Provides a mechanism to build and install external projects as part of your build process. Useful when the external project's build system is incompatible. **Example:** """cmake include(ExternalProject) ExternalProject_Add( googletest GIT_REPOSITORY "https://github.com/google/googletest.git" GIT_TAG "v1.14.0" CMAKE_ARGS -DCMAKE_INSTALL_PREFIX=${CMAKE_BINARY_DIR}/googletest-install -DBUILD_SHARED_LIBS=OFF BUILD_TESTS 0 INSTALL_COMMAND ${CMAKE_COMMAND} -E make_directory "${CMAKE_BINARY_DIR}/googletest-install" && ${CMAKE_COMMAND} -E copy_directory "${CMAKE_BINARY_DIR}/googletest/googletest/include" "${CMAKE_BINARY_DIR}/googletest-install/include" && ${CMAKE_COMMAND} -E copy_if_different "${CMAKE_BINARY_DIR}/googletest/lib/libgtest.a" "${CMAKE_BINARY_DIR}/googletest-install/lib/libgtest.a" && ${CMAKE_COMMAND} -E copy_if_different "${CMAKE_BINARY_DIR}/googletest/lib/libgtest_main.a" "${CMAKE_BINARY_DIR}/googletest-install/lib/libgtest_main.a" ) add_library(googletest INTERFACE) target_include_directories(googletest INTERFACE "${CMAKE_BINARY_DIR}/googletest-install/include") target_link_directories(googletest INTERFACE "${CMAKE_BINARY_DIR}/googletest-install/lib") add_executable(my_tests test.cpp) target_link_libraries(my_tests googletest gtest gtest_main) add_dependencies(my_tests googletest) """ ### 2.4. Package Configuration Files **Standard:** Create and use Package Configuration Files ("<PackageName>Config.cmake") when distributing libraries or applications that other CMake projects will depend on. **Do This:** Use "configure_file()" to generate the configuration file from a template. Use "install()" to copy the configuration file to the correct location. **Don't Do This:** Require users to manually configure include directories and link libraries. **Why:** Simplifies the integration of your library into other CMake projects. Makes dependency management more robust and user-friendly. **Example "MyLibraryConfig.cmake.in":** """cmake # MyLibraryConfig.cmake.in # Config file for MyLibrary # Set the install prefix if it's not already set if (NOT DEFINED MyLibrary_INSTALL_PREFIX) set(MyLibrary_INSTALL_PREFIX "@CMAKE_INSTALL_PREFIX@") endif() # Set the include directory set(MyLibrary_INCLUDE_DIR "@PACKAGE_INCLUDE_INSTALL_DIR@") # Set the library location set(MyLibrary_LIBRARY "@PACKAGE_LIBRARY_INSTALL_DIR@/libmylibrary.a") # Create the MyLibrary::MyLibrary target include(CMakeParseArguments) CMakeParseArguments(PARSE_ARGV 0 MyLibrary_ARGS "" "INCLUDE_DIRS;LIBS" "" ) include(FindPackageHandleStandardArgs) FIND_PACKAGE_HANDLE_STANDARD_ARGS(MyLibrary REQUIRED_VARS MyLibrary_LIBRARY MyLibrary_INCLUDE_DIR ) if(NOT TARGET MyLibrary::MyLibrary) add_library(MyLibrary::MyLibrary UNKNOWN IMPORTED) SET_TARGET_PROPERTIES(MyLibrary::MyLibrary PROPERTIES INTERFACE_INCLUDE_DIRECTORIES "${MyLibrary_INCLUDE_DIR}" IMPORTED_LOCATION "${MyLibrary_LIBRARY}" # or SHARED_LIBRARY INTERFACE_COMPILE_FEATURES "cxx_std_17") # Or appropriate C++ feature level endif() """ **Example "CMakeLists.txt" (Configuring and Installing the Config File):** """cmake # Set install locations set(PACKAGE_INCLUDE_INSTALL_DIR "include") set(PACKAGE_LIBRARY_INSTALL_DIR "lib") # Configure the config file configure_file( MyLibraryConfig.cmake.in ${CMAKE_BINARY_DIR}/MyLibraryConfig.cmake ) # Install the config file install(FILES ${CMAKE_BINARY_DIR}/MyLibraryConfig.cmake DESTINATION "lib/cmake/MyLibrary" # creates lib/cmake/MyLibrary/MyLibraryConfig.cmake ) # Install header files install(DIRECTORY include/ DESTINATION ${PACKAGE_INCLUDE_INSTALL_DIR} ) install(TARGETS MyLibrary DESTINATION ${PACKAGE_LIBRARY_INSTALL_DIR}) """ ## 3. API Key and Secret Management ### 3.1. Securely Passing API Keys **Standard:** Never store API keys or secrets directly in your source code. Pass them as environment variables or use a secure configuration file. **Do This:** Use "ENV{API_KEY}" or a configuration file loaded at runtime. **Don't Do This:** Hardcode API keys in "CMakeLists.txt" or source code, even for tests. **Why:** Prevents accidental exposure of sensitive credentials in version control systems or build artifacts. **Example:** """cmake # CMakeLists.txt add_executable(my_app main.cpp) #Pass API Key as a compile definition target_compile_definitions(my_app PRIVATE "API_KEY=$ENV{API_KEY}") #main.cpp #include <iostream> #define STR_VALUE(arg) #arg #define API_KEY_STRING(name) STR_VALUE(name) int main() { std::cout << API_KEY_STRING(API_KEY) << std::endl; return 0; } """ ### 3.2. Configuration Files **Standard:** When needing more complex configuration, use a JSON or YAML file included with the application. **Do this:** Use libraries such as "nlohmann_json" or "YAML-CPP" to parse configuration files at runtime. **Don't Do this:** Store sensitive information without proper encryption or access controls. **Why:** Provides a flexible and manageable way to handle different environments and configurations. **Example:** """json // config.json { "api_key": "YOUR_API_KEY", "api_url": "https://example.com/api" } """ """cpp // main.cpp #include <iostream> #include <fstream> #include <nlohmann/json.hpp> using json = nlohmann::json; int main() { std::ifstream f("config.json"); json data = json::parse(f); std::string apiKey = data["api_key"]; std::string apiUrl = data["api_url"]; std::cout << "API Key: " << apiKey << std::endl; std::cout << "API URL: " << apiUrl << std::endl; return 0; } """ ## 4. Versioning and Compatibility ### 4.1. API Versioning **Standard:** When possible depend on package version, to future proof against breaking API changes. **Do This:** Use the "VERSION" or "COMPATIBILITY" options of "find_package". Employ semantic versioning in your own libraries. **Don't Do This:** Assume that APIs will remain constant forever, or hardcode version-specific logic without a clear strategy. **Why:** Allows for gradual upgrades and avoids breaking changes in existing projects. **Example:** """cmake find_package(MyAPI 2.0 REQUIRED) # Require version 2.0 or later """ ### 4.2. Conditional Compilation **Standard:** Use conditional compilation ("if" statements) based on API version or feature availability. **Do This:** Check for version variables set by "find_package()" or feature flags defined by the API. Use "target_compile_definitions" to enable or disable code based on these conditions. **Don't Do This:** Create excessively complex or deeply nested conditional compilation blocks. Refactor into separate functions or classes if necessary. **Why:** Allows you to support multiple API versions or variations within a single codebase. **Example:** """cmake find_package(MyAPI REQUIRED) if (MyAPI_VERSION VERSION_GREATER "2.0") target_compile_definitions(my_app PRIVATE "MYAPI_VERSION_2_0_OR_LATER") endif() #In the code #ifdef MYAPI_VERSION_2_0_OR_LATER //logic code here #endif """ ## 5. Error Handling and Logging ### 5.1. Robust Error Handling **Standard:** Implement thorough error handling at all levels of API interaction. **Do This:** Catch exceptions, check return codes, and log errors with informative messages. Use CMake's "message()" command with appropriate severity levels (STATUS, WARNING, ERROR, FATAL_ERROR). **Don't Do This:** Silently ignore errors or allow exceptions to propagate unhandled. **Why:** Makes debugging easier, prevents unexpected crashes, and provides users with helpful feedback. **Example:** """cpp #include <iostream> #include <stdexcept> // Assume this function makes a network call and can throw an exception std::string callAPI(const std::string& url) { try { // ... API call logic ... throw std::runtime_error("API server is unavailable."); // Simulate an error } catch (const std::exception& e) { std::cerr << "Error calling API: " << e.what() << std::endl; throw; // Re-throw the exception to be handled higher up } } """ """cmake # CMakeLists.txt try_run(API_WORKS "" "" "${CMAKE_CURRENT_SOURCE_DIR}/api_test.cpp" COMPILE_OUTPUT_VARIABLE compile_result RUN_OUTPUT_VARIABLE run_result) if (API_WORKS) message ("API Available!") else () message (FATAL_ERROR "API Unavailable! $compile_result $run_result") endif () """ ### 5.2. Comprehensive Logging **Standard:** Implement detailed logging in API integration modules to track activity and diagnose issues. **Do This:** Use CMake's "message()" command with appropriate log levels (STATUS, DEBUG). Consider integrating with a logging library in your C++ code. **Don't Do This:** Log sensitive information (API keys, passwords) or overwhelm the user with excessive logging. **Why:** Makes debugging and troubleshooting easier, especially in complex or distributed systems. **Example:** """cmake message(STATUS "Fetching dependency from ${GIT_REPOSITORY}...") """ ## 6. Testing API Integrations ### 6.1. Unit Tests **Standard:** Write unit tests for your API wrapper classes to verify their correctness and robustness. **Do This:** Use a testing framework like Google Test or Catch2. Mock or stub out the actual API calls to isolate the wrapper logic. **Don't Do This:** Skip unit tests for API integrations. **Why:** Ensures that your API wrapper behaves as expected and catches errors early. Simplifies refactoring and maintenance. ### 6.2. Integration Tests **Standard:** Implement integration tests to verify the interaction between your application and the external API. **Do This:** Set up a test environment that closely resembles the production environment. Use a test API key or account. Limit the scope of integration tests to specific scenarios. **Don't Do This:** Run integration tests against live production APIs. **Why:** Verifies that your application can successfully interact with the external API in a realistic environment. ## 7. Security Best Practices ### 7.1. Input Validation **Standard:** Validate all input received from the API, including data types, ranges, and formats. **Do This:** Use appropriate validation techniques in your C++ code (e.g., range checks, regular expressions). **Don't Do This:** Trust that the API will always return valid data. **Why:** Prevents attacks such as buffer overflows, SQL injection, and cross-site scripting (XSS). ### 7.2. Data Sanitization **Standard:** Sanitize all data before displaying it to users or storing it in a database. **Do This:** Use appropriate sanitization techniques in your C++ code (e.g., HTML escaping, URL encoding). **Don't Do This:** Display raw data from the API without sanitization. **Why:** Prevents XSS attacks and other vulnerabilities. ## 8. Performance Optimization ### 8.1. Caching API Responses **Standard:** Implement caching to reduce the number of API calls. **Do This:** Use a caching library (e.g., Redis, Memcached) or implement a simple in-memory cache. **Don't Do This:** Cache sensitive information without proper encryption or access controls. **Why:** Improves performance, reduces latency, and reduces the load on the external API. ### 8.2. Asynchronous API Calls **Standard:** Use asynchronous API calls to avoid blocking the main thread. **Do This:** Use C++ concurrency features (e.g., "std::async", "std::future") or an asynchronous networking library. **Don't Do This:** Make synchronous API calls on the main thread, especially in GUI applications. **Why:** Improves responsiveness and prevents the application from freezing. ## 9. Platform Independence ### 9.1. Cross-Platform Builds **Standard:** Ensure the API integration code compiles and runs correctly on all supported platforms. **Do This:** Use CMake's platform-specific features (e.g., "CMAKE_SYSTEM_NAME", "CMAKE_SYSTEM_PROCESSOR") to adapt the build process to different platforms. Use conditional compilation to handle platform-specific API differences. **Don't Do This:** Rely on platform-specific libraries or features without providing a fallback for other platforms. **Why:** Allows you to build and deploy your application on a wide range of platforms.