# Tooling and Ecosystem Standards for CMake
This document outlines coding standards and best practices specifically related to tooling and the ecosystem surrounding CMake projects. Adhering to these standards will promote consistency, maintainability, and efficiency within CMake-based projects.
## 1. IDE and Editor Configuration
### 1.1. Standard: Use an IDE or Editor with CMake Support
**Do This:** Use an IDE such as Visual Studio (with CMake Tools), CLion, or VS Code with CMake extensions (e.g., CMake Tools by Microsoft). Ensure your editor supports CMake syntax highlighting, code completion, and integrated build/debug workflows.
**Don't Do This:** Rely solely on text editors without CMake-specific features.
**Why:** CMake projects benefit enormously from IDE integration, which provides features like:
* Syntax highlighting and error detection
* Code completion
* Integrated build and debug workflows
* CMake target browsing
**Example (VS Code settings.json):**
"""json
{
"cmake.configureOnOpen": true,
"cmake.configureSettings": {
"CMAKE_TOOLCHAIN_FILE": "${workspaceFolder}/cmake/toolchain.cmake",
"CMAKE_BUILD_TYPE": "Debug"
},
"cmake.generator": "Ninja Multi-Config",
"files.associations": {
"*.cmake": "cmake"
}
}
"""
### 1.2. Standard: Utilize CMake Language Server Protocol (LSP)
**Do This:** Configure your editor to use a CMake Language Server (e.g., supplied by the CMake Tools extension).
**Don't Do This:** Manually manage CMake syntax checking or code completion.
**Why:** LSP support provides real-time diagnostics, code completion, and formatting based on the CMake language specification. This helps catch errors early and improves code quality.
**Example (VS Code):** The CMake Tools extension automatically handles LSP.
### 1.3. Standard: Consistent Project Toolchain
**Do this:** Use a toolchain file for specifying compiler, linker, and other build tools, especially when cross-compiling or using specific compiler versions.
**Don't do this:** Rely on CMake's automatic toolchain detection if you have specific or complex requirements.
**Why:** Toolchain files enable reproducible builds across different environments. They abstract the toolchain details from the CMakeLists.txt files, making the project more portable.
**Example (cmake/toolchain.cmake):**
"""cmake
# Compiler settings
set(CMAKE_SYSTEM_NAME Generic)
set(CMAKE_SYSTEM_PROCESSOR arm)
# Specify the cross compiler
set(CMAKE_C_COMPILER /opt/toolchains/arm-none-eabi-gcc/bin/arm-none-eabi-gcc)
set(CMAKE_CXX_COMPILER /opt/toolchains/arm-none-eabi-gcc/bin/arm-none-eabi-g++)
# Specify flags for the compiler
set(CMAKE_C_FLAGS "-march=armv7-a -mfloat-abi=hard -mfpu=neon" CACHE STRING "C Compiler Flags" FORCE)
set(CMAKE_CXX_FLAGS "${CMAKE_C_FLAGS}" CACHE STRING "C++ Compiler Flags" FORCE)
set(CMAKE_FIND_ROOT_PATH /opt/toolchains/arm-none-eabi-gcc/arm-none-eabi)
# Search for programs only in the specified directory
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM ONLY)
# Search for libraries and headers only in the specified directory
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
"""
## 2. Static Analysis and Code Quality
### 2.1. Standard: Integrate CMake Linting
**Do This:** Use a CMake linter like "cmakelint", "cppcheck", or "CodeQL" to automatically analyze CMake code for potential errors, style violations, and security vulnerabilities. Integrate the linter into your CI/CD pipeline.
**Don't Do This:** Manually review CMake code for these issues.
**Why:** Linters automate the process of identifying potential problems, leading to higher-quality and more maintainable CMake code.
**Example (using "cmakelint"):**
1. Install: "pip install cmakelint"
2. Run: "cmakelint CMakeLists.txt"
3. Integrate with pre-commit hooks (see below)
### 2.2. Standard: Implement Pre-Commit Hooks
**Do This:** Use a pre-commit hook system (e.g., "pre-commit") to automatically run linters and formatters before committing changes.
**Don't Do This:** Rely on manual checks or CI systems to catch formatting and linting issues *after* committing.
**Why:** Pre-commit hooks prevent bad code from ever entering the repository, ensuring consistent code quality.
**Example (.pre-commit-config.yaml):**
"""yaml
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.5.0
hooks:
- id: check-yaml
- id: end-of-file-fixer
- id: trailing-whitespace
- repo: https://github.com/cpp-best-practices/pre-commit-hooks
rev: v0.5.0
hooks:
- id: cmake-format
- id: cmakelint
"""
### 2.3. Standard: Code Formatting
**Do This:** Use a CMake code formatter like "cmake-format" to automatically format CMake code according to a consistent style. Configure the formatter to use your team's style guidelines.
**Don't Do This:** Rely on manual formatting or inconsistent styles.
**Why:** Consistent code formatting improves readability and reduces merge conflicts.
**Example (.cmake-format.yaml):**
"""yaml
line_width: 100
tab_size: 4
use_tabchars: false
dangle_parens: true
separate_ctrl_name_with_space: true
command_case: lower
"""
Run: "cmake-format -i CMakeLists.txt"
## 3. Dependency Management
### 3.1. Standard: Use CPM.cmake or FetchContent for External Dependencies
**Do This:** Prefer modern dependency management solutions like "CPM.cmake" or CMake's built-in "FetchContent" module for handling external dependencies.
**Don't Do This:** Rely on system-installed libraries or manually downloaded dependencies.
**Why:** These methods automate the process of downloading, configuring, and building dependencies, ensuring reproducible builds. "CPM.cmake" is preferred for complicated projects involving multiple, evolving libraries. "FetchContent" is preferred for simpler, less frequently changed external dependencies.
**Example (CPM.cmake):**
"""cmake
include(CPM)
CPMAddPackage("ghc::fmt"
VERSION "9.1.0"
GITHUB_REPOSITORY "fmtlib/fmt"
)
add_executable(my_program main.cpp)
target_link_libraries(my_program ghc::fmt)
"""
**Example (FetchContent):**
"""cmake
include(FetchContent)
FetchContent_Declare(
googletest
GIT_REPOSITORY https://github.com/google/googletest.git
GIT_TAG v1.13.0
)
FetchContent_MakeAvailable(googletest)
add_executable(my_tests test.cpp)
target_link_libraries(my_tests gtest gtest_main)
"""
### 3.2. Standard: Manage Transitive Dependencies Carefully
**Do This:** Be aware of transitive dependencies (dependencies of your dependencies). Use "PUBLIC", "INTERFACE", and "PRIVATE" visibility settings correctly to control which dependencies are propagated to下游 targets. Consider using "find_package" with the "REQUIRED" keyword to ensure your dependencies are available.
**Don't Do This:** Blindly link against all dependencies without considering their visibility. This can lead to unnecessary dependencies and increased build times.
**Why:** Proper dependency management reduces build times, avoids symbol conflicts, and simplifies debugging.
**Example:**
"""cmake
add_library(mylib mylib.cpp)
target_link_libraries(mylib PUBLIC fmt) # fmt is a public dependency of mylib
add_executable(my_app main.cpp)
target_link_libraries(my_app mylib) # my_app automatically gets fmt as a dependency
"""
### 3.3. Standard: Version Pinning
**Do This:** Pin the versions of your dependencies to specific commits, tags, or versions to ensure reproducible builds and prevent unexpected breakages due to dependency updates.
**Don't Do This:** Use floating tags or branches (e.g., "latest") for dependencies.
**Why:** Version pinning ensures that your project always builds with the same versions of its dependencies, preventing compatibility issues and build failures.
**Example (CPM.cmake):** See previous "CPMAddPackage" example.
**Example (FetchContent):** See previous "FetchContent_Declare" example using "GIT_TAG".
## 4. Testing and Continuous Integration
### 4.1. Standard: Integrate with a Testing Framework
**Do This:** Use a testing framework such as Google Test, Catch2, or doctest to write unit tests and integration tests for your code.
**Don't Do This:** Rely on manual testing or ad-hoc test scripts.
**Why:** Testing frameworks provide a structured way to write and run tests, making it easier to ensure the correctness and reliability of your code.
**Example (Google Test):** See previous "FetchContent" example demonstrating Google Test integration. Further setup would involve writing tests in "test.cpp".
### 4.2. Standard: CTest Integration
**Do This:** Use CMake's built-in "CTest" testing framework to manage and run your tests. Use "add_test" to register tests and "enable_testing" to enable testing.
**Don't Do This:** Run tests manually or outside of the "CTest" framework.
**Why:** "CTest" provides a standard way to run tests and report results, which is essential for CI/CD integration.
**Example:**
"""cmake
enable_testing()
add_executable(my_tests test.cpp)
target_link_libraries(my_tests gtest gtest_main)
add_test(NAME MyTests COMMAND my_tests)
"""
### 4.3. Standard: Continuous Integration
**Do This:** Integrate your CMake project with a CI/CD system such as GitHub Actions, GitLab CI, Azure DevOps, or Jenkins. Configure the CI system to automatically build and test your code on every commit and pull request.
**Don't Do This:** Rely on manual builds and tests.
**Why:** CI/CD automates the build and test process, providing rapid feedback on code changes and ensuring that the code is always in a working state.
**Example (.github/workflows/cmake.yml):** (GitHub Actions)
"""yaml
name: CMake
on:
push:
branches: [ "main" ]
pull_request:
branches: [ "main" ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install Dependencies
run: sudo apt-get update && sudo apt-get install -y libgtest-dev
- name: Configure CMake
run: cmake -B ${{github.workspace}}/build -DCMAKE_BUILD_TYPE=Debug
- name: Build
run: cmake --build ${{github.workspace}}/build
- name: Run Tests
run: cd ${{github.workspace}}/build && ctest
"""
### 4.4. Standard: Code Coverage Analysis
**Do This:** Integrate code coverage analysis tools (e.g., "gcov", "lcov", "llvm-cov") to measure the percentage of code covered by your tests. Set coverage goals and monitor coverage trends.
**Don't Do This:** Ignore code coverage or rely solely on passing tests.
**Why:** Code coverage analysis helps identify areas of code that are not adequately tested, allowing you to improve the quality and reliability of your tests.
**Example (with "lcov" on Linux):**
1. Build with coverage flags: "-DCMAKE_CXX_FLAGS="-g -O0 --coverage""
2. Run tests.
3. Generate coverage data: "lcov -capture -directory . -output-file coverage.info"
4. Create HTML report: "genhtml coverage.info -output-directory coverage_report"
## 5. Packaging and Distribution
### 5.1. Standard: CPack Integration
**Do This:** Utilize CMake's built-in "CPack" utility to generate installers and packages for your project. Support multiple package formats (e.g., DEB, RPM, NSIS, DMG) for different platforms.
**Don't Do This:** Manually create installers or packages.
**Why:** "CPack" automates the packaging process, ensuring consistent and reproducible packages across different platforms.
**Example:**
"""cmake
include(InstallRequiredSystemLibraries)
set(CPACK_PROJECT_NAME ${PROJECT_NAME})
set(CPACK_PROJECT_VERSION ${PROJECT_VERSION})
include(CPack)
"""
Configure CPack variables to customize the packaging process (e.g., "CPACK_GENERATOR", "CPACK_PACKAGE_DESCRIPTION", "CPACK_PACKAGE_LICENSE"). Run "cpack" to generate the packages.
### 5.2. Standard: Export Targets
**Do This:** Use "install(TARGETS)" with the "EXPORT" option and "export()" command to create CMake package configuration files. These files allow other CMake projects to easily find and use your library or application as a dependency.
**Don't Do This:** Require users to manually find headers and libraries.
**Why:** Exported targets simplify dependency management and make it easier for other projects to use your library or application.
**Example:**
"""cmake
install(TARGETS mylib
EXPORT mylib-export
DESTINATION lib
)
install(
FILES mylib.h
DESTINATION include
)
export(TARGETS mylib
FILE mylib-config.cmake
)
"""
### 5.3. Standard: Semantic Versioning
**Do This:** Follow semantic versioning (SemVer) for your project's releases. Increment the major version when you make incompatible API changes, the minor version when you add functionality in a backwards compatible manner, and the patch version when you make backwards compatible bug fixes.
**Don't Do This:** Use arbitrary versioning schemes or inconsistent versioning practices.
**Why:** Semantic versioning provides a clear and consistent way to communicate the nature of changes in your project, allowing users to understand the potential impact of upgrading.
## 6. Tooling Wrappers
### 6.1. Standard: Abstract tool invocations
**Do This:** Create CMake functions or macros that wrap common tool invocations like code generation, documentation building, or custom build steps.
**Don't Do This:** Directly embed tool commands within "add_custom_command" without abstraction.
**Why:** Encapsulating tool invocations improves readability, maintainability, and reusability across the project. If the underlying tool changes, only the wrapper needs updating.
**Example:**
"""cmake
function(generate_proto_code input_proto output_cpp output_header)
find_program(PROTOC REQUIRED) # Ensure Protobuf compiler is found
add_custom_command(
OUTPUT ${output_cpp} ${output_header}
COMMAND ${PROTOC}
ARGS --cpp_out=${CMAKE_CURRENT_SOURCE_DIR}
--proto_path=${CMAKE_CURRENT_SOURCE_DIR}
${input_proto}
DEPENDS ${input_proto}
COMMENT "Generating protobuf code from ${input_proto}"
)
add_custom_target(generate_${input_proto} DEPENDS ${output_cpp} ${output_header})
set_source_files_properties(${output_cpp} ${output_header} PROPERTIES GENERATED TRUE)
endfunction()
# Usage
generate_proto_code(my_proto.proto my_proto.pb.cc my_proto.pb.h)
add_library(my_library my_proto.pb.cc)
add_dependencies(my_library generate_my_proto.proto)
"""
### 6.2. Standard: Error Handling in Tool Wrappers
**Do This:** Implement proper error handling within your tool wrappers. Check the return code of the tool and provide informative error messages if the tool fails.
**Don't Do This:** Ignore the return code of the tool.
**Why:** Proper error handling helps diagnose problems when external tools fail. Without it, the build might continue with corrupted or incomplete output, leading to further errors down the line.
**Example:**
"""cmake
function(execute_custom_tool command_line)
execute_process(
COMMAND ${command_line}
RESULT_VARIABLE result
OUTPUT_VARIABLE output
ERROR_VARIABLE error
)
if(NOT ${result} EQUAL "0")
message(FATAL_ERROR "Custom tool failed with error code ${result}:\nOutput: ${output}\nError: ${error}")
endif()
endfunction()
"""
These standards and examples will help to create a strong and maintainable build environment for CMake projects, enabling developers to focus on the core functionality rather than build intricacies. Using modern approaches provides a solid foundation for scalable and collaborative software development.
danielsogl
Created Mar 6, 2025
This guide explains how to effectively use .clinerules
with Cline, the AI-powered coding assistant.
The .clinerules
file is a powerful configuration file that helps Cline understand your project's requirements, coding standards, and constraints. When placed in your project's root directory, it automatically guides Cline's behavior and ensures consistency across your codebase.
Place the .clinerules
file in your project's root directory. Cline automatically detects and follows these rules for all files within the project.
# Project Overview project: name: 'Your Project Name' description: 'Brief project description' stack: - technology: 'Framework/Language' version: 'X.Y.Z' - technology: 'Database' version: 'X.Y.Z'
# Code Standards standards: style: - 'Use consistent indentation (2 spaces)' - 'Follow language-specific naming conventions' documentation: - 'Include JSDoc comments for all functions' - 'Maintain up-to-date README files' testing: - 'Write unit tests for all new features' - 'Maintain minimum 80% code coverage'
# Security Guidelines security: authentication: - 'Implement proper token validation' - 'Use environment variables for secrets' dataProtection: - 'Sanitize all user inputs' - 'Implement proper error handling'
Be Specific
Maintain Organization
Regular Updates
# Common Patterns Example patterns: components: - pattern: 'Use functional components by default' - pattern: 'Implement error boundaries for component trees' stateManagement: - pattern: 'Use React Query for server state' - pattern: 'Implement proper loading states'
Commit the Rules
.clinerules
in version controlTeam Collaboration
Rules Not Being Applied
Conflicting Rules
Performance Considerations
# Basic .clinerules Example project: name: 'Web Application' type: 'Next.js Frontend' standards: - 'Use TypeScript for all new code' - 'Follow React best practices' - 'Implement proper error handling' testing: unit: - 'Jest for unit tests' - 'React Testing Library for components' e2e: - 'Cypress for end-to-end testing' documentation: required: - 'README.md in each major directory' - 'JSDoc comments for public APIs' - 'Changelog updates for all changes'
# Advanced .clinerules Example project: name: 'Enterprise Application' compliance: - 'GDPR requirements' - 'WCAG 2.1 AA accessibility' architecture: patterns: - 'Clean Architecture principles' - 'Domain-Driven Design concepts' security: requirements: - 'OAuth 2.0 authentication' - 'Rate limiting on all APIs' - 'Input validation with Zod'
# Testing Methodologies Standards for CMake This document outlines the coding standards for testing methodologies in CMake projects. It covers unit, integration, and end-to-end testing strategies specifically within the CMake context, emphasizing modern approaches and best practices. ## 1. General Testing Principles ### 1.1 Clarity and Organization * **Do This:** Structure tests clearly, with descriptive names for test files, test targets, and individual test cases. Aim for self-documenting tests that explain WHAT is being tested and WHY. * **Don't Do This:** Use cryptic or abbreviated names that obscure the purpose of the test. Avoid large, monolithic test files that test too many unrelated components. * **Why:** Clarity enhances maintainability. Well-organized tests are easier to understand, debug, and extend, preventing test rot and promoting confidence in the codebase. """cmake # Good: add_test(NAME MyLibrary_StringUtil_Trim_LeadingWhitespace COMMAND ${CMAKE_CTEST_COMMAND} -C $<CONFIG> -R "MyLibrary_StringUtil_Trim_LeadingWhitespace" ) # Bad: add_test(NAME test1 COMMAND ${CMAKE_CTEST_COMMAND} -C $<CONFIG> -R "test1") """ ### 1.2 Test-Driven Development (TDD) * **Do This:** Consider adopting TDD principles, writing tests *before* implementing the code that satisfies them. * **Don't Do This:** Write tests as an afterthought. Tests should drive the design and implementation of the feature. * **Why:** TDD encourages better design, reduces bugs, and leads to more testable code. It also provides immediate feedback on changes. ### 1.3 Independent Tests * **Do This:** Ensure each test is independent from other tests. Tests should not rely on the side effects of previous tests. * **Don't Do This:** Create test suites with dependencies between tests. This makes debugging and reasoning about failures extremely difficult and can lead to cascading issues. * **Why:** Independence prevents cascading failures and makes tests easier to reason about and debug. ### 1.4 Test Data Management * **Do This:** Use realistic and relevant test data, but avoid complex or excessive data that obscures the test's purpose. Consider generating test data programmatically for complex scenarios. * **Don't Do This:** Hardcode unrealistic or minimal test data that doesn't reflect real-world use cases. * **Why:** Realistic test data increases confidence that the code works in production-like environments. ## 2. Unit Testing ### 2.1 Framework Selection * **Do This:** Use a modern C++ testing framework like Google Test, Catch2, or doctest. Integrate the framework into your CMake project using "FetchContent" or "find_package". * **Don't Do This:** Roll your own testing framework. Rely on "printf"-style debugging or manual inspection. * **Why:** Established frameworks provide powerful features like assertions, test organization, mocking, and reporting. """cmake # Example using FetchContent and Google Test (gtest) include(FetchContent) FetchContent_Declare( googletest GIT_REPOSITORY https://github.com/google/googletest.git GIT_TAG v1.14.0 # Use a specific version ) FetchContent_MakeAvailable(googletest) add_executable(MyLibraryTests test/MyLibraryTests.cpp) target_link_libraries(MyLibraryTests MyLibrary gtest_main) include(GoogleTest) gtest_discover_tests(MyLibraryTests) """ ### 2.2 Test Structure * **Do This:** Organize unit tests into well-defined test cases targeting specific functions or classes. Use a common pattern for test case naming (e.g., "ClassName_MethodName_Scenario"). * **Don't Do This:** Mix multiple tests within a single large test function. Use overly broad test cases that try to test everything at once. * **Why:** Fine-grained tests pinpoint the source of failures more easily than blanket tests. """cpp // Example using Google Test #include "MyLibrary.h" #include "gtest/gtest.h" TEST(StringUtil_Trim_LeadingWhitespace, EmptyString) { std::string str = ""; MyLibrary::trim(str); ASSERT_EQ(str, ""); } TEST(StringUtil_Trim_LeadingWhitespace, StringWithLeadingSpaces) { std::string str = " Hello"; MyLibrary::trim(str); ASSERT_EQ(str, "Hello"); } """ ### 2.3 Mocking and Isolation * **Do This:** Use mocking frameworks (e.g., Google Mock) to isolate units under test from external dependencies (e.g., databases, network services, hardware). Prefer dependency injection over global state. * **Don't Do This:** Directly access external resources or rely on global state within unit tests. This makes tests slow, non-deterministic, and difficult to run in isolation. * **Why:** Mocking ensures that tests are fast, repeatable, and focus on the behavior of the unit under test. ### 2.4 CMake Integration * **Do This:** Create a separate executable target for unit tests. Use "add_test" to register the tests with CTest. Leverage "ctest_discover_tests" to automatically discover tests. * **Don't Do This:** Embed test code directly within library or application code. Manually invoke test executables. * **Why:** Separating tests allows for clean separation of concerns and enables automated test execution within the CMake build process. "ctest_discover_tests" significantly simplifies test management. """cmake # Modern CMake: Automating with "ctest_discover_tests" add_executable(MyLibraryTests test/MyLibraryTests.cpp) target_link_libraries(MyLibraryTests MyLibrary gtest_main) include(GoogleTest) gtest_discover_tests(MyLibraryTests) # Automatically find and run tests """ ## 3. Integration Testing ### 3.1 Scope * **Do This:** Focus integration tests on verifying the interactions between multiple components or modules. Aim to test the interfaces and data flows between units. * **Don't Do This:** Use integration tests to replicate unit tests (testing individual functions in isolation). * **Why:** Integration tests ensure that the parts of a system work together correctly, catching issues that unit tests might miss. ### 3.2 Environments * **Do This:** Define integration test environments with clear dependencies and configurations. Automate the setup and teardown of these environments. * **Don't Do This:** Run integration tests against production environments. Use inconsistent or manually configured environments. * **Why:** Predictable environments make integration tests reliable and repeatable. ### 3.3 Data Persistence * **Do This:** If testing code that interacts with databases or file systems, use dedicated testing databases or directories. Clear and reset the data after each test. * **Don't Do This:** Write to production databases or modify important files during testing. * **Why:** Avoid data corruption or unintended side effects on other systems. ### 3.4 CMake Integration * **Do This:** Package integration tests as separate executables. Use "add_test" to register these tests with CTest, similar to unit tests. Consider using CMake's "execute_process" command to control external processes or services during testing. * **Don't Do This:** Manually run integration tests or rely on external scripts outside of the CMake build process. * **Why:** Integrate into Continuous Integration (CI/CD) pipelines for automatic execution and reporting. """cmake # Example integration test using CMake's "execute_process" add_executable(IntegrationTests test/IntegrationTests.cpp) target_link_libraries(IntegrationTests MyLibrary) # Defines custom target to start a mock service (replace with actual service) add_custom_target(start_mock_service COMMAND ${CMAKE_COMMAND} -E echo "Starting mock service..." COMMAND ${CMAKE_COMMAND} -E sleep 1 # Simulate startup time WORKING_DIRECTORY ${CMAKE_BINARY_DIR} ) # Defines custom target to stop the mock service (replace with actual service) add_custom_target(stop_mock_service COMMAND ${CMAKE_COMMAND} -E echo "Stopping mock service..." COMMAND ${CMAKE_COMMAND} -E sleep 1 WORKING_DIRECTORY ${CMAKE_BINARY_DIR} ) # Integration test definition add_test( NAME IntegrationTest_MyFeature COMMAND ${CMAKE_COMMAND} -E env "MOCK_SERVICE_URL=http://localhost:8080" $<TARGET_FILE:IntegrationTests> WORKING_DIRECTORY ${CMAKE_BINARY_DIR} DEPENDS start_mock_service stop_mock_service #Run start and stop custom targets ) set_property(TEST IntegrationTest_MyFeature PROPERTY ENVIRONMENT "MOCK_ENABLED=1") #Example Environment var """ ## 4. End-to-End (E2E) Testing ### 4.1 Scope * **Do This:** Design E2E tests to simulate real user workflows, exercising the entire system from input to output. These tests validate that all system components work together to deliver the desired functionality. * **Don't Do This:** Focus on low-level details or internal component interactions. Treat the system as a black box. * **Why:** E2E tests provide the highest level of confidence that the system works as expected from a user's perspective. ### 4.2 Test Automation * **Do This:** Automate E2E tests using tools like Selenium, Cypress, or custom-built scripts. Integrate these tests into your CI/CD pipeline. * **Don't Do This:** Rely on manual E2E testing or sporadic, ad-hoc testing. Requires fully automated, repeatable execution. * **Why:** Automation ensures consistent and reliable E2E testing, enabling rapid feedback on changes. ### 4.3 Environment Configuration * **Do This:** Ensure the E2E test environment closely mimics the production environment, including infrastructure, configuration, and data. Consider using containerization technologies (e.g., Docker) to create consistent environments. * **Don't Do This:** Test against development or staging environments that differ significantly from production. * **Why:** Production-like environments maximize the likelihood of detecting issues before they reach users. ### 4.4 Test Data Management * **Do This:** Seed the E2E test environment with representative data to simulate real-world usage. Clean up and reset the data after each test run. * **Don't Do This:** Use minimal or unrealistic data that doesn't expose potential issues. * **Why:** Realistic data ensures the E2E tests accurately reflect the behavior of the system in production. ### 4.5 CMake Integration * **Do This:** Create CMake custom commands to manage E2E test setup, execution, and teardown. Use "add_test" to integrate these commands with CTest. Employ "execute_process" to run external test tools or scripts. * **Don't Do This:** Run E2E tests outside of the CMake build process. * **Why:** CMake integration allows for seamless E2E test execution within your build workflow. """cmake # Example E2E test using CMake and a hypothetical E2E test script add_custom_command( OUTPUT ${CMAKE_BINARY_DIR}/e2e_test_result.txt COMMAND ${CMAKE_COMMAND} -E echo "Running End-to-End Tests..." COMMAND python ${CMAKE_SOURCE_DIR}/test/e2e_tests.py --output ${CMAKE_BINARY_DIR}/e2e_test_result.txt DEPENDS MyApplication # Assuming MyApplication needs to be built first WORKING_DIRECTORY ${CMAKE_BINARY_DIR} # Set working directory correctly ) add_test(NAME EndToEndTest COMMAND ${CMAKE_COMMAND} -E true #Test always "passes", result is parsed from file. Update status appropriately. DEPENDS ${CMAKE_BINARY_DIR}/e2e_test_result.txt #Ensure Command is run. ) """ ## 5. Test Reporting and Analysis ### 5.1 CTest Integration * **Do This:** Use CTest's reporting capabilities to generate test summaries and dashboards. Configure CTest to upload test results to a CI/CD server (e.g., Jenkins, GitLab CI, GitHub Actions) or a dedicated test reporting tool (e.g., CDash). * **Don't Do This:** Ignore CTest's reporting features or rely on manual inspection of test output. * **Why:** CTest provides centralized test management and reporting, enabling you to track test results, identify trends, and quickly diagnose failures. ### 5.2 Code Coverage * **Do This:** Integrate code coverage tools (e.g., gcov, lcov) into your CMake build process. Use these tools to measure the percentage of code covered by your tests. Strive for high code coverage to minimize the risk of undetected bugs. * **Don't Do This:** Neglect code coverage analysis or assume that tests adequately cover all code paths. * **Why:** Code coverage analysis helps you identify gaps in your testing and improve the overall quality of your codebase. ### 5.3 Static Analysis * **Do This:** Integrate static analysis tools (e.g., Clang Static Analyzer, cppcheck) into your CMake build process. Use these tools to identify potential code defects, security vulnerabilities, and style violations. * **Don't Do This:** Ignore static analysis warnings or suppress them without careful consideration. * **Why:** Static analysis can detect issues early in the development cycle, before they become more costly to fix. ## 6. Avoiding Common Anti-Patterns ### 6.1 "Magic Strings" and Hardcoded Values * **Don't Do This:** Use "magic strings" or hardcoded values in test code. * **Do This:** Define constants or configuration parameters for test data, file paths, and other values. This is a common pattern. * **Why:** Improves readability and maintainability. ### 6.2 Over-Reliance on Mocks * **Don't Do This:** Mock everything. * **Do This:** Use mocks strategically to isolate units under test, but also include integration tests to verify real interactions. * **Why:** Balance unit and integration testing. ### 6.3 Ignoring Failing Tests * **Don't Do This:** Ignore failing tests or disable them without fixing the underlying issues. * **Do This:** Treat failing tests as high-priority bugs. * **Why:** Prevents test rot and ensures code quality. ### 6.4 Testing Implementation Details * **Don't Do This:** Write tests that are tightly coupled to the implementation details of the code they test. * **Do This:** Focus on testing the public interface and the expected behavior of the code. * **Why:** Reduces test fragility and makes it easier to refactor code without breaking tests.
# 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.
# Performance Optimization Standards for CMake This document outlines best practices for optimizing CMake build configuration files to improve build times, reduce resource consumption, and enhance overall development workflow performance. These guidelines focus on using modern CMake features (CMake 3.15+) and avoiding common pitfalls. ## 1. Minimize Included Files and Dependencies The first principle of efficient CMake configurations is to reduce the amount of parsing and processing CMake needs to do. This primarily revolves around limiting the number of files included and keeping the dependency tree lean. ### 1.1. Reducing the Number of "include()" Calls **Do This:** Limit the number of "include()" calls. Refactor common functionality into modules that are only included when needed by specific targets. **Don't Do This:** Avoid blanket "include()" statements at the top level of your "CMakeLists.txt". This forces CMake to parse irrelevant files for every target. **Why:** Unnecessary "include()" calls introduce overhead by forcing CMake to parse and execute code that may not be relevant to the current target. **Example:** """cmake # Anti-pattern: including everything globally # include(Utilities) # Parses Utilities.cmake even if not needed # Better pattern: Target-specific inclusion function(add_my_library name) add_library(${name} ...) target_sources(${name} ...) # Only include Utilities if this target needs it. Utilies.cmake can expose # cmake functions in this scenario that are only available for targets that include it. include(Utilities OPTIONAL) if(DEFINED Utilities_FOUND) # Call Utility functions utility_function(${name}) endif() endfunction() add_my_library(MyLibrary) """ ### 1.2. Interface Libraries for Header-Only Dependencies **Do This:** Use "INTERFACE" libraries to propagate header-only dependencies and preprocessor definitions. **Don't Do This:** Copy-paste compiler flags and include directories across multiple targets. **Why:** "INTERFACE" libraries provide a clean and efficient way to manage dependencies without requiring target linking or source code compilation. This avoids unnecessary rebuilds when only header files change. **Example:** """cmake # Create an interface library for a header-only dependency add_library(MyHeaderOnlyLib INTERFACE) # Specify the include directories needed by MyHeaderOnlyLib target_include_directories(MyHeaderOnlyLib INTERFACE $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include> $<INSTALL_INTERFACE:include> ) # Optionally, add preprocessor definitions target_compile_definitions(MyHeaderOnlyLib INTERFACE MY_HEADER_ONLY_LIB_ENABLED ) # Link against this interface library add_executable(MyExecutable main.cpp) target_link_libraries(MyExecutable PRIVATE MyHeaderOnlyLib) """ ### 1.3. Package Handling and Dependencies **Do This:** Use "find_package()" with the "CONFIG" mode and version requirements. Leverage CMake packages fully. **Don't Do This:** Manually specify include paths and library locations when a proper CMake package exists. **Why:** "find_package()" with "CONFIG" mode offloads dependency resolution to the imported package's CMake configuration files, often providing optimized build settings. This also allows cleaner cross-platform support. Modern CMake makes consumption of 3rd party libraries significantly easier using targets. **Example:** """cmake # Correct: Using find_package with required version and COMPONENTS find_package(Boost 1.70 REQUIRED COMPONENTS system filesystem) if (Boost_FOUND) add_executable(MyProgram main.cpp) target_link_libraries(MyProgram Boost::system Boost::filesystem) #Use IMPORTED targets endif() # Anti-pattern: Manual specification (prone to errors and platform-specific issues) # include_directories(/path/to/boost/include) # link_libraries(/path/to/boost/lib/libboost_system.a) """ ### 1.4 Conditional Dependencies **Do This:** Make dependencies optional and conditional using "find_package()" with the "REQUIRED" keyword omitted. Consider using components with "find_package" to minimize extraneous dependencies **Don't Do This:** Force hard dependencies when a feature can degrade gracefully without them **Why:** Reducing the number of mandatory dependencies lightens the dependency closure and improves configuration time, especially when some dependencies involve significant external processing during the CMake stage (e.g., external project inclusion or code generation). **Example:** """cmake find_package(OptionalDependency) if(OptionalDependency_FOUND) # Link to the optional dependency target_link_libraries(MyTarget PUBLIC OptionalDependency::OptionalDependency) # Note imported namespace target # ... use optional dependency features else() message(STATUS "OptionalDependency not found, some features will be disabled") # ... disable features that depend on the optional dependency target_compile_definitions(MyTarget PRIVATE "NO_OPTIONAL_DEPENDENCY") endif() """ ## 2. Optimize Compiler and Linker Flags Strategic selection and use of compiler and linker flags can drastically improve code generation, link times, and runtime performance. ### 2.1. Position Independent Executables (PIE) **Do This:** Enable Position Independent Executables (PIE) for executables, especially on modern Linux systems, and use Address Space Layout Randomization (ASLR). **Don't Do This:** Disable PIE or ASLR without a strong, justified reason. **Why:** PIE and ASLR enhance security by making it harder for attackers to exploit memory corruption vulnerabilities. **Example:** """cmake set(CMAKE_POSITION_INDEPENDENT_CODE ON) # Enable for all targets # OR, target-specific set_target_properties(MyExecutable PROPERTIES POSITION_INDEPENDENT_CODE ON ) """ ### 2.2. Link-Time Optimization (LTO) **Do This:** Enable LTO for release builds to allow the compiler to perform optimizations across translation units. **Don't Do This:** Use LTO indiscriminately for debug builds, as it can significantly increase compile times. **Why:** LTO can yield significant performance improvements by enabling inter-procedural optimizations, dead code elimination, and function inlining across the entire program. **Example:** """cmake # Enable LTO for Release builds if (CMAKE_BUILD_TYPE STREQUAL "Release") set(CMAKE_INTERPROCEDURAL_OPTIMIZATION CHECKED) #CHECKED allows user overwrite via ccmake or cmake-gui endif() """ ### 2.3. Optimize for Target Architecture **Do This:** Set compiler flags to optimize code for the target CPU architecture (e.g., "-march=native" for GCC/Clang). Use CMake generator expressions to set architecture specific flags, for example, when cross compiling. **Don't Do This:** Use generic optimization flags that don't take advantage of specific CPU features. **Why:** Architecture-specific optimizations can improve performance by utilizing instruction sets and features available on the target CPU. **Example:** """cmake if(CMAKE_SYSTEM_PROCESSOR MATCHES "x86_64") target_compile_options(MyTarget PRIVATE "-march=native") # Optimize for the build machine's CPU elseif(CMAKE_SYSTEM_PROCESSOR MATCHES "arm64") target_compile_options(MyTarget PRIVATE "-march=armv8-a") endif() """ ### 2.4. Precompiled Headers (PCH) **Do This:** Use precompiled headers to reduce compilation time by pre-compiling common header files. **Don't Do This:** Neglect using PCHs on large codebases with slow includes. **Why:** PCH avoids redundant compilation of frequently included headers, significantly reducing build times. **Example:** """cmake # Create a precompiled header set_source_files_properties(MyProject/pch.h PROPERTY COMPILE_LANGUAGE CXX) add_library(MyProjectPCH OBJECT MyProject/pch.h) target_compile_options(MyProjectPCH PRIVATE -x c++-header) # Adjust for your compiler # Use the precompiled header target_precompile_headers(MyTarget PUBLIC MyProject/pch.h) # Requires CMake >= 3.16 #alternative for CMake < 3.16: #target_sources(MyTarget PRIVATE MyProject/source1.cpp MyProject/source2.cpp) #set_source_files_properties( # ${CMAKE_CURRENT_SOURCE_DIR}/source1.cpp # ${CMAKE_CURRENT_SOURCE_DIR}/source2.cpp # PROPERTIES # COMPILE_FLAGS "-include pch.h" #) """ ## 3. Efficient CMake Code Structures The structure and organization of CMake code directly impact build times. Avoiding unnecessary operations and using efficient constructs reduces the configuration overhead. ### 3.1. Minimize String Operations **Do This:** Avoid extensive string manipulation within CMake scripts. For example avoid excessive generation of paths and lists. **Don't Do This:** Perform complex string processing that will be invoked on every configuration. **Why:** CMake string operations can be slow, especially when dealing with large strings or complex regexes. **Example:** """cmake # Bad: Repeated string appending/manipulation set(MY_PATH "${CMAKE_SOURCE_DIR}") set(MY_PATH "${MY_PATH}/src") set(MY_PATH "${MY_PATH}/module1") # Better: Directly construct the path if possible. set(MY_PATH "${CMAKE_SOURCE_DIR}/src/module1") """ ### 3.2. Use "set(CACHE)" Sparingly **Do This:** Only use "set(CACHE)" for variables that need to persist across CMake invocations, and use them BEFORE "project()" **Don't Do This:** Overuse "set(CACHE)", as it can slow down the configuration process. **Why:** Cache variables are re-read from disk on every CMake run, which incurs overhead. Limit their use to persistent configuration options. **Example:** """cmake # Good pattern: Defining cache variables before project() set(MY_OPTION "default_value" CACHE STRING "Description of the option") project(MyProject) # Within a function or scope within CMake set(LOCAL_VARIABLE "value") # Not cached """ ### 3.3. Efficient List Operations **Do This:** Use CMake's built-in list manipulation commands efficiently. Only iterate when necessary. **Don't Do This:** Iterate over lists unnecessarily or perform redundant list operations. **Why:** Inefficient list handling degrades CMake performance, especially on large projects with many source files or dependencies. **Example:** """cmake # Bad: Inefficient loop set(MY_FILES "file1.cpp;file2.cpp;file3.cpp") foreach(FILE ${MY_FILES}) message(STATUS "Processing ${FILE}") # Unnecessarily iterates endforeach() # Better: Process the entire list at once when possible message(STATUS "All files: ${MY_FILES}") """ ### 3.4. Variable Scoping **Do This:** Leverage variable scoping (functions, blocks) to avoid variable pollution in the global scope. **Don't Do This:** Use global variables unnecessarily, as these can lead to conflicts and unintended side effects, slowing down debugging and adding complexity. **Why:** Scoping improves code clarity, reduces the risk of name collisions, and allows CMake to manage variable lifetimes more efficiently. **Example:** """cmake function(my_function) set(local_variable "value") # Local to the function message(STATUS "Local variable: ${local_variable}") endfunction() my_function() #message(STATUS "Local variable outside function: ${local_variable}") # Error: variable not defined """ ### 3.5 Generator Expressions **Do This:** Use Generator expressions to defer complex logic to generator time. This is especially crucial if the dependency checking involves many checks. **Don't Do This:** Perform potentially costly logic evaluation during CMake configuration when the information is only needed at generate time. **Why:** Generator expressions are evaluated during build system generation, so it can evaluate system information later during generate step rather than at configuration. **Example:** """cmake add_executable(my_app main.cpp) target_compile_definitions(my_app PRIVATE "DEBUG_MODE=$<$<CONFIG:Debug>:TRUE;FALSE>" # Evaluated during compile step rather than evaluate the if statement in CMake config ) """ ## 4. Parallel Builds and Ninja Generator Embrace parallel builds and modern build systems to reduce compilation time. ### 4.1. Utilizing Parallel Compilation **Do This:** Encourage users to use the "-j" flag with "make" or specify the number of jobs in IDEs to enable parallel compilation. **Don't Do This:** Artificially limit the number of parallel jobs or fail to inform users about the benefits of parallel compilation. **Why:** Parallel compilation significantly reduces build times, especially on multi-core processors. **Example:** Instruct users to build with: "make -j$(nproc)" ### 4.2. Ninja Generator **Do This:** Use the Ninja generator, especially for large projects, as it is generally faster than the "make" generator. **Don't Do This:** Stick with the "make" generator without considering the potential performance benefits of Ninja. **Why:** Ninja is designed for speed, with a focus on incremental builds and parallel execution. It tends to outperform "make" in many scenarios. **Example:** """bash cmake -G Ninja . ninja """ ### 4.3. Ccache Integration **Do This:** Integrate "ccache" to speed up recompilation by caching compiled object files. **Don't Do This:** Ignore "ccache" in environments where frequent recompilation occurs. **Why:** "ccache" reduces build times by reusing previously compiled object files when the source code and compiler flags haven't changed. **Example:** """cmake # Add ccache to the compiler command line (requires ccache to be installed) find_program(CCACHE_PROGRAM ccache) if(CCACHE_PROGRAM) set_property(GLOBAL PROPERTY RULE_LAUNCH_COMPILE "${CCACHE_PROGRAM}") endif() """ ## 5. Modularization and Abstraction Breaking CMake configurations into logical modules simplifies maintenance and promotes reuse. ### 5.1. Custom Modules and Functions **Do This:** Refactor common build logic into reusable custom modules (".cmake" files) and functions. **Don't Do This:** Duplicate CMake code across multiple "CMakeLists.txt" files. **Why:** Modularization makes CMake configurations easier to understand, maintain, and extend and reduces parsing load to increase CMake configuration time. **Example:** """cmake # MyModule.cmake function(add_my_executable name) add_executable(${name} ${ARGN}) target_compile_features(${name} PUBLIC cxx_std_17) endfunction() # CMakeLists.txt include(MyModule) add_my_executable(MyProgram main.cpp) """ ### 5.2. Abstract Configuration Options **Do This:** Abstract complex configuration options behind simple, user-friendly variables with meaningful defaults. **Don't Do This:** Expose internal implementation details directly to the user. **Why:** Abstraction simplifies the configuration process, reduces the risk of errors, and allows you to change the underlying implementation without breaking user scripts. **Example:** """cmake # Good: Abstracted option option(ENABLE_FEATURE "Enable the super cool feature" ON) # Bad: Exposing implementation details # set(FEATURE_IMPL "internal_lib;dependency1;dependency2") # Hard to understand/maintain """ ### 5.3. Policy Management **Do This:** Explicitly set CMake policies using "cmake_policy()" to ensure consistent behavior across CMake versions and avoid deprecation warnings. **Don't Do This:** Rely on default policy settings, as these may change in future CMake versions. **Why:** Setting policies explicitly ensures that your CMake code behaves predictably and is forward-compatible. **Example:** """cmake cmake_minimum_required(VERSION 3.15) # Set the minimum required version before setting policies cmake_policy(SET CMP0077 NEW) # Example policy (use actual policy ID) """ By adhering to these coding standards, you can create CMake build configuration files that are performant, maintainable, and robust. This leads to faster build times, a smoother development workflow, and improved overall software quality. Remember to keep abreast of the latest CMake features and best practices by regularly consulting the official CMake documentation.