# Deployment and DevOps Standards for CMake
This document outlines CMake coding standards focused on deployment and DevOps aspects. It provides guidelines for structuring CMake projects to facilitate robust build processes, seamless integration with CI/CD pipelines, and optimized production deployments. These standards are geared towards modern CMake practices and emphasize best practices for maintainability, performance, and security.
## 1. Build Process Standardization
### 1.1. Consistent Build Types
**Standard:** Enforce consistent build types across development, testing, and production environments.
* **Do This:** Use "CMAKE_BUILD_TYPE" and a consistent set of flags (e.g., "-DCMAKE_BUILD_TYPE=Release", "-DCMAKE_BUILD_TYPE=Debug")
* **Don't Do This:** Hardcode compilation flags directly within "CMakeLists.txt" without considering build types.
**Why:** Ensures predictable behavior and avoids discrepancies between environments.
**Example:**
"""cmake
# Set default build type if not specified
if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
set(CMAKE_BUILD_TYPE Release CACHE STRING
"Choose the type of build, options are: Debug Release RelWithDebInfo MinSizeRel."
FORCE)
endif()
# Compiler flags based on build type
if(CMAKE_BUILD_TYPE STREQUAL "Debug")
message(STATUS "Debug build type selected")
add_compile_options(-g -O0)
elseif(CMAKE_BUILD_TYPE STREQUAL "Release")
message(STATUS "Release build type selected")
add_compile_options(-DNDEBUG -O3)
endif()
"""
### 1.2. Out-of-Source Builds
**Standard:** Always use out-of-source builds to prevent polluting source directories.
* **Do This:** Create a separate build directory (e.g., "build", "out"). Run CMake from this directory.
* **Don't Do This:** Run CMake directly in the source directory.
**Why:** Keeps the source tree clean and allows for multiple independent builds with different configurations.
**Example:**
"""bash
mkdir build
cd build
cmake ..
make
"""
### 1.3. Package Management Integration
**Standard:** Integrate with package managers (e.g., Conan, vcpkg) for dependency handling.
* **Do This:** Use "find_package" with package manager configuration files. Consider using CMake FetchContent for simple external dependencies.
* **Don't Do This:** Manually download and include dependencies directly in the source tree.
**Why:** Simplifies dependency management, improves reproducibility, and enables version control of dependencies.
**Example (Using FetchContent):**
"""cmake
include(FetchContent)
FetchContent_Declare(
googletest
GIT_REPOSITORY https://github.com/google/googletest.git
GIT_TAG v1.13.0
)
# For versions of CMake older than 3.14, use FetchContent_GetProperties
# instead of FetchContent_Populate
FetchContent_Populate(googletest)
if(NOT googletest_SOURCE_DIR)
FetchContent_MakeAvailable(googletest)
endif()
include_directories(${googletest_SOURCE_DIR}/googletest/include)
add_executable(my_tests test.cpp)
target_link_libraries(my_tests GTest::gtest_main)
"""
### 1.4. Minimizing External Dependencies
**Standard:** Keep external dependencies to a minimum, preferring standard library features where appropriate.
* **Why:** This reduces the risk of dependency conflicts, build failures, and security vulnerabilities. It also improves build times and reduces the overall complexity of the deployment process.
* **Do This:** Carefully evaluate the necessity of each dependency. Consider writing small, self-contained functions to replace external libraries where feasible.
* **Don't Do This:** Add dependencies without a clear justification. Assume that a dependency provides better performance or functionality without benchmarking and profiling.
## 2. CI/CD Integration
### 2.1. Configuration-as-Code
**Standard:** Define build and deployment configurations in code (e.g., YAML files, shell scripts).
* **Do This:** Store CI/CD configurations (e.g., ".gitlab-ci.yml", "Jenkinsfile") alongside the "CMakeLists.txt".
* **Don't Do This:** Manually configure CI/CD pipelines through web interfaces.
**Why:** Enables version control, auditability, and reproducibility of the entire build and deployment process.
**Example (.gitlab-ci.yml):**
"""yaml
stages:
- build
- test
- deploy
build:
stage: build
script:
- mkdir build
- cd build
- cmake ..
- make
test:
stage: test
dependencies:
- build
script:
- cd build
- ./my_tests
deploy:
stage: deploy
dependencies:
- test
script:
- echo "Deploying application..."
- # Deployment commands here
only:
- main
"""
### 2.2. Test Automation
**Standard:** Automate testing as part of the CI/CD pipeline.
* **Do This:** Use "add_test" and CTest to define and run tests.
* **Don't Do This:** Rely solely on manual testing.
**Why:** Ensures code quality, detects regressions early, and provides confidence in deployments.
**Example:**
"""cmake
enable_testing()
add_executable(my_tests test.cpp)
target_link_libraries(my_tests gtest_main)
include(GoogleTest)
gtest_discover_tests(my_tests) #Recommended for modern CMake. Will auto-detect tests.
add_test(NAME MyTest COMMAND my_tests) #Legacy method of adding tests
"""
### 2.3. Artifact Management
**Standard:** Manage build artifacts (e.g., executables, libraries) using version control and artifact repositories.
* **Do This:** Use staging directories for artifacts, then upload to an artifact repository (e.g., Artifactory, Nexus) during CI/CD.
* **Don't Do This:** Directly deploy from the build environment without versioning artifacts.
**Why:** Enables rollback capabilities, provides a central location for binary sharing, and facilitates auditing.
**Example (CMake for packaging):**
"""cmake
include(InstallRequiredSystemLibraries)
include(GNUInstallDirs) #Recommended modern approach for finding standard install locations
install(TARGETS my_executable
DESTINATION ${CMAKE_INSTALL_BINDIR}) # Use CMAKE_INSTALL_BINDIR for /usr/local/bin
install(DIRECTORY assets/
DESTINATION ${CMAKE_INSTALL_DATADIR}/my_application) # Use CMAKE_INSTALL_DATADIR for /usr/local/share
"""
### 2.4. Conditional Compilation for Environments
**Standard:** Use conditional compilation and preprocessor directives to configure code behavior based on the target environment (development, testing, production).
* **Do This:** Define macros or variables based on the environment and use them in the code. For example, use "-DDEBUG" or "-DPRODUCTION" flags passed to the compiler.
* **Don't Do This:** Hardcode environment-specific configurations directly into the code without any mechanism for conditional behavior.
**Why:** Allows for customized behavior like verbose logging in development or optimized performance in production.
**Example:** CMakeLists.txt
"""cmake
if (CMAKE_BUILD_TYPE STREQUAL "Debug")
add_definitions(-DDEBUG)
else()
add_definitions(-DNDEBUG)
endif()
"""
C++ Code:
"""c++
#ifdef DEBUG
std::cout << "Debug message: Value = " << value << std::endl;
#endif
"""
## 3. Production Considerations
### 3.1. Optimization Flags
**Standard:** Use appropriate optimization flags for production builds.
* **Do This:** Use "-O3 -DNDEBUG" for Release builds. Profile the application and tune optimization flags accordingly. Consider using link-time optimization (LTO).
* **Don't Do This:** Use "-g -O0" flags in production.
**Why:** Improves performance and reduces binary size in production.
**Example:**
"""cmake
if(CMAKE_BUILD_TYPE STREQUAL "Release")
add_compile_options(-O3 -DNDEBUG)
endif()
"""
### 3.2. Static Analysis
**Standard:** Integrate static analysis tools (e.g., Clang Static Analyzer, SonarQube) into the CI/CD pipeline.
* **Do This:** Run static analysis on every commit and fail the build if critical issues are found. Address findings promptly.
* **Don't Do This:** Ignore static analysis warnings.
**Why:** Identifies potential bugs, security vulnerabilities, and code quality issues early in the development lifecycle. Makes sure you have a clean build that's ready for production.
**Example (basic clang-tidy):**
"""cmake
find_program(CLANG_TIDY NAMES clang-tidy)
if(CLANG_TIDY)
set_property(SOURCE ${SOURCES} PROPERTY CXX_CLANG_TIDY "${CLANG_TIDY}")
message(STATUS "clang-tidy found: ${CLANG_TIDY}")
else()
message(WARNING "clang-tidy not found.")
endif()
"""
### 3.3. Minimizing Binary Size
**Standard:** Reduce the size of the final binary for production deployments.
* **Do This:** Enable link-time optimization (LTO), strip debugging symbols, and use appropriate compiler flags. Remove unused code and dependencies.
* **Don't Do This:** Include unnecessary debugging information or unused libraries in production binaries.
**Why:** Smaller binaries can be deployed more quickly, consume less storage space, and potentially improve load times.
**Example (LTO):**
"""cmake
set(CMAKE_INTERPROCEDURAL_OPTIMIZATION TRUE) # Enables LTO for Release and RelWithDebInfo
"""
### 3.4 Security Hardening
**Standard:** Implement security hardening measures.
* **Do This:** Enable compiler flags like "-fstack-protector-strong", "-D_FORTIFY_SOURCE=2", and use address sanitizers during testing. Regularly audit dependencies for vulnerabilities. Ensure proper user permissions.
* **Don't Do This:** Deploy with default configurations and without security considerations.
**Why:** Protects against common security exploits such as buffer overflows and format string vulnerabilities.
**Example:**
"""cmake
if(CMAKE_BUILD_TYPE STREQUAL "Release")
add_compile_options(
-fstack-protector-strong
-D_FORTIFY_SOURCE=2
)
add_link_options(-Wl,-z,relro,-z,now) #Enable RELRO and NOW
endif()
"""
### 3.5. Resource Management
**Standard:** Implement strategies for effective resource management (CPU, Memory, Disk I/O)
* **Do This:** Use resource limits, profiling tools to optimize bottlenecks, and efficient data structures to minimize memory usage. Implement proper error handling and resource cleanup procedures.
* **Don't Do This:** Leak resources, perform excessive I/O operations, or fail to monitor resource consumption in production.
**Why:** Ensures that the application runs reliably and efficiently. Reduces risk of resource exhaustion and improves overall system stability.
## 4. Modern CMake and DevOps
### 4.1. Using Modules Effectively
**Standard:** Leverage CMake's module system for code reuse and maintainability.
* **Do This:** Create custom modules for common tasks and project-specific logic. Consider contributing reusable modules to the CMake community.
* **Don't Do This:** Duplicate code across multiple "CMakeLists.txt" files.
**Why:** Promotes modularity, reduces code duplication, and improves maintainability.
**Example (Creating a custom module):**
"""cmake
# In Modules/MyProjectHelpers.cmake
function(my_custom_function target)
# ... implementation ...
endfunction()
"""
"""cmake
# In CMakeLists.txt
include(MyProjectHelpers)
my_custom_function(my_target)
"""
### 4.2. Export Targets
**Standard:** Use "install(TARGETS)" and "export" commands to create easily consumable packages.
* **Do This:** Export targets to allow other projects to easily link against your libraries.
* **Don't Do This:** Require consumers to manually find and link against dependencies.
**Why:** Simplifies dependency management and promotes code reuse.
**Example:**
"""cmake
install(TARGETS my_library
EXPORT my_library_export
DESTINATION lib)
install(
EXPORT_SETTING_FILE
DESTINATION lib/cmake/MyLibrary
EXPORT_NAME MyLibraryTargets
)
# In CMakeLists.txt of consuming project:
find_package(MyLibrary REQUIRED)
target_link_libraries(my_consumer MyLibrary::my_library)
"""
### 4.3. Toolchain Files
**Standard:** Utilize toolchain files for cross-compilation and specialized environments.
* **Do This:** Create toolchain files to specify the compiler, linker, and other tools for a specific target platform.
* **Don't Do This:** Hardcode compiler paths and flags directly in "CMakeLists.txt".
**Why:** Enables cross-compilation and simplifies building for different target architectures.
**Example (Toolchain file - my_toolchain.cmake):**
"""cmake
set(CMAKE_SYSTEM_NAME Generic)
set(CMAKE_SYSTEM_PROCESSOR arm)
set(CMAKE_C_COMPILER arm-linux-gnueabi-gcc)
set(CMAKE_CXX_COMPILER arm-linux-gnueabi-g++)
set(CMAKE_FIND_ROOT_PATH /opt/arm-linux-gnueabi)
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY)
#Usage: cmake -DCMAKE_TOOLCHAIN_FILE=my_toolchain.cmake ..
"""
### 4.4. Handling Different Operating Systems
**Standard:** Use CMake commands to handle differences in operating systems and compiler versions gracefully.
* **Do This:** Use "if(WIN32)", "if(UNIX)", and similar checks, along with "CMAKE_CXX_COMPILER_ID", but prefer feature detection where possible.
* **Don't Do This:** Use platform-specific code without proper checks.
**Why:** Ensures the project can be built and deployed correctly across different environments.
**Example:**
"""cmake
if(WIN32)
add_definitions(-D_WIN32)
endif()
if(CMAKE_CXX_COMPILER_ID MATCHES "Clang")
message(STATUS "Using Clang compiler")
endif()
"""
## 5. Anti-Patterns and Common Mistakes
### 5.1. Over-Reliance on "execute_process"
**Anti-Pattern:** Excessive use of "execute_process" for tasks that CMake can handle natively.
* **Why:** Reduces portability, increases complexity, and can introduce security vulnerabilities. "execute_process" should be a last resort.
* **Alternatives:** Use CMake's built-in commands for file manipulation, dependency management, and build system generation.
### 5.2. Ignoring CMAKE_EXPORT_COMPILE_COMMANDS
**Anti Pattern:** Not setting "CMAKE_EXPORT_COMPILE_COMMANDS" to "TRUE".
* **Why:** Prevents IDEs and code analysis tools from properly indexing code, thus hindering development effectiveness.
* **Solution:** Set "set(CMAKE_EXPORT_COMPILE_COMMANDS TRUE)" to generate compilation databases to give IDEs access to flags and compile definitions necessary for proper code analytics and code completion.
### 5.3. Complex Custom Commands
**Anti-Pattern:** Creating overly complex custom commands.
* **Why:** They can become difficult to maintain and debug. They can severely slow down the build if executed unnecessarily.
* **Solution:** Consider refactoring them into CMake functions or scripts. Use DEPENDS clauses to ensure correct execution order.
### 5.4. Neglecting Documentation
**Anti-Pattern:** Lack of documentation for CMake code.
* **Why:** Makes it difficult for other developers to understand and maintain the build system.
* **Solution:** Add comments to explain the purpose of CMake code, especially complex logic. Use Markdown to document the project structure and build process.
### 5.5 Using "MESSAGE" for important information or warnings.
* **Why:** "Message" statements work, but are not as effective as built-in methods.
* **Solution:** Utilize "STATUS", "WARNING", and "FATAL_ERROR" message types to clearly communicate the severity and intent of messages.
This document provides a foundational set of standards and best practices for deployment and DevOps with CMake. Following these guidelines will lead to more robust, maintainable, and secure projects. Remember to adapt these standards to your specific project requirements and technology stack.
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.