As C++ projects grow, they often include files that aren’t part of the source code but remain essential—files like README.md
, LICENSE.md
, and configuration files such as .clang-format
. Managing these non-source files in a CMake project can feel inconsistent across teams and repositories. Today, let’s propose a clean, standardized approach to include such files: introducing a dedicated config target.
The Problem: Non-Source Files Are Often Ignored
CMake excels at organizing build systems, but it doesn’t directly provide a way to group or manage non-source files. By default, these files live alongside the source code but are often:
- Ignored by IDEs (e.g., CLion, Visual Studio).
- Left out of CMake’s dependency tree.
- Omitted when analyzing project structure.
This inconsistency makes it harder to standardize project configurations, especially when handing off projects or onboarding new team members.
Wouldn’t it be better if we had a predictable, organized approach to managing these files?
The Solution: A config Target
We propose a config target—an elegant, standardized way to collect and “register” non-source files at the top-level CMakeLists.txt. This target consolidates important but often overlooked project artifacts, improving clarity across tools and teams.
Here’s how it works:
add_custom_target(config SOURCES ${CMAKE_SOURCE_DIR}/README.md ${CMAKE_SOURCE_DIR}/LICENSE.md ${CMAKE_SOURCE_DIR}/vcpkg.json ${CMAKE_SOURCE_DIR}/.clang-format ${CMAKE_SOURCE_DIR}/.gitignore )
Breaking Down the config Target
Why add_custom_target
?
The add_custom_target
command creates a logical target in CMake that doesn’t produce an output file (like a binary or library). This makes it perfect for grouping non-buildable files like documentation, licenses, or configuration files.
Using the SOURCES
Keyword
By adding these files as SOURCES
, you ensure that IDEs and tools recognize them as part of the project. For instance:
- CLion will display these files in its project tree.
- Visual Studio will include them in the Solution Explorer.
- Code analysis tools or scripts can iterate over all project files predictably.
Centralized Management
With the config
target in the top-level CMakeLists.txt
, it’s easy to add, modify, or remove non-source files in one place. Teams no longer need to search multiple directories to check if all essential files are present.
Integration and Tooling Benefits: Automating the config Target
The config
target isn’t just about organization; it opens the door for automation and tooling. Defining a consistent target for non-source files enables scripts and tools to interact with these files predictably.
Here’s how automation enhances the config
target:
Validation Scripts
You can write scripts to ensure the config
target always contains required project artifacts. For example, a pre-commit hook or CI job can verify that files like LICENSE.md
or .clang-format
are present and up-to-date:
#!/bin/bash # Validate 'config' target contents required_files=("README.md" "LICENSE.md" "vcpkg.json" ".clang-format" ".gitignore") for file in "${required_files[@]}"; do if [ ! -f "$file" ]; then echo "Error: Missing $file in the project root." exit 1 fi done echo "All config files are present."
You can add this script to your CI pipeline or version control hooks to enforce consistency automatically.
Custom Build Scripts
Since the config
target explicitly lists these files, custom scripts can iterate over the target to perform automated tasks. For example:
- Generating tarballs: Include all non-source files when packaging.
- License enforcement: Automatically inject license headers into code if
LICENSE.md
changes. - Format validation: Ensure
.clang-format
or.clang-tidy
files are included, and the code style is validated accordingly.
With CMake, you can trigger such scripts directly:
add_custom_command(TARGET config POST_BUILD COMMAND ${CMAKE_COMMAND} -E echo "Validating config files..." COMMAND ${CMAKE_COMMAND} -E touch ${CMAKE_BINARY_DIR}/config_validated )
Tooling Integration
Tools like CPack and Ninja can leverage the config
target for packaging or dependency checks:
- CPack: Include the
config
target files in distributed archives (tarballs, zip files). - Static analysis tools: Iterate through the
config
target to validate dependencies or detect misconfigurations.
The automation potential grows as your project scales. By defining a clear config
target, you enable tools and scripts to treat non-source files with the same level of consistency as build artifacts.
Avoiding Name Collisions
If you are concerned with having multiple config
targets within your workspace, and these names conflict with each other, you can easily prefix the name as part of your standard. Here, I suggest using the built-in ${PROJET_NAME}
variable. (This helps enforce automation and supports auto-coding.):
add_custom_target(${PROJECT_NAME}-config SOURCES ${CMAKE_SOURCE_DIR}/README.md ${CMAKE_SOURCE_DIR}/LICENSE.md ${CMAKE_SOURCE_DIR}/vcpkg.json ${CMAKE_SOURCE_DIR}/.clang-format ${CMAKE_SOURCE_DIR}/.gitignore )
The config Target as a Project Standard: A Reasonable Default
A standard, like the config
target promotes consistency across projects and teams. When every project includes this target, you gain a reliable default structure for project configurations.
Consistency Across Teams
When teams agree to use the config
target, they achieve:
- Predictable Project Layouts: Every C++ project follows the same structure for non-source files.
- Simpler Onboarding: New developers instantly understand where to find key files like
README.md
and.clang-format
. - Easier Reviews and Maintenance: Code reviews and CI pipelines can verify that the
config
target aligns with project standards.
Teams no longer need to reinvent the wheel for handling configuration files. Instead, they inherit a reasonable default that works across tools and environments.
Improved Testing and Validation
By centralizing non-source files into a dedicated target, testing and validation workflows become more robust. You can:
- Ensure Mandatory Files Exist: CI jobs can validate the presence and contents of required artifacts.
- Automate Style Checks: Include
.clang-format
or.clang-tidy
checks in your build pipeline. - Verify Documentation Updates: Tools can scan
README.md
orCHANGELOG.md
for required updates after a code change.
A standardized config
target simplifies testing these workflows across multiple projects.
Scalable Project Maintenance
For large codebases, maintenance often suffers from scattered, untracked artifacts. A centralized config
target eliminates this issue. Artifacts like LICENSE.md
and .clang-format
become explicit, documented parts of the project structure.
By adopting this pattern, you also future-proof your projects. Adding or removing files is straightforward, with changes visible in a single, top-level CMakeLists.txt
file.
The config Target as a Reasonable Default
The config
target represents a reasonable default for managing non-source files in CMake projects. It’s minimal, easy to implement, and highly extensible. More importantly, it provides a clear convention that teams can adopt across projects.
Imagine every C++ repository in your organization having this predictable structure:
- A top-level
CMakeLists.txt
defines theconfig
target. - Scripts, CI jobs, and tools integrate seamlessly with these files.
- New team members quickly onboard, seeing familiar project conventions.
With this simple addition, you make non-source files a first-class citizen of your project.
Final Thoughts
Standardizing the handling of non-source files through a config
target is a small yet impactful change. It simplifies project management, improves tooling automation, and enhances consistency across teams.
By treating this approach as a reasonable default, you encourage a culture of organized, predictable C++ projects—making them easier to build, test, validate, and maintain.
Is this a convention you’d consider for your projects? Share your thoughts or improvements in the comments!
Discover more from John Farrier
Subscribe to get the latest posts sent to your email.
If you follow this advice, your project can’t be consumed via FetchContent by any other project that follows the same advice. Non-imported targets must be globally unique, so if the consuming project already created a
config
target, the dependency project would cause an error when it also tried to create aconfig
target.Great to know! Perhaps the
config
target name could be appended with the project name, in that case? Or do you have another suggestion for improving this solution?