Working with the File System
Exploring CMake's file()
command, covering how to read, write, copy, and glob files, and how to manipulate paths.
So far, our build scripts have lived in a self-contained world. They define targets, link libraries, and set options, but they haven't needed to interact much with the world outside their own CMakeLists.txt
files.
Real-world build processes, however, are rarely so isolated. They often need to:
- Read configuration data from external files.
- Find source files automatically.
- Generate code or configuration headers.
- Copy assets and resources into the build directory.
To accomplish these tasks, our scripts need to be able to read, write, and manipulate files and directories on the disk. This lesson introduces the primary tool for all such operations: the versatile file()
command.
Working with Paths
Before we work with files, it's worth understanding how to work with their paths. CMake provides several built-in variables and a get_filename_component()
command for this purpose.
Essential Path Variables
We've seen some of these before, but it's worth summarizing them here. These variables help you refer to key locations in your project in a portable way:
CMAKE_SOURCE_DIR
/PROJECT_SOURCE_DIR
: The absolute path to the top-level source directory (where the rootCMakeLists.txt
is). These variables are often the same, but there is a subtle difference within builds that contain multipleproject()
commands. We'll cover this later.CMAKE_BINARY_DIR
/PROJECT_BINARY_DIR
: CMake uses the "binary directory" name for what most developers call the build directory. These variables stores the absolute path to the top-level build directory - where you ran thecmake
command. Again, they are usually the same, but there is a subtle difference when we have multipleproject()
commands.CMAKE_CURRENT_SOURCE_DIR
: The absolute path to the source directory containing theCMakeLists.txt
file that is currently being processed. This will be different fromCMAKE_SOURCE_DIR
when it is used in aCMakeLists.txt
file that was loaded through theadd_subdirectory()
command.CMAKE_CURRENT_BINARY_DIR
: The absolute path to the build directory corresponding to the current source directory. Again, this will be different toCMAKE_BINARY_DIR
within theadd_subdirectory()
context.
Manipulating Paths with get_filename_component()
This command is CMake's primary tool for dissecting and manipulating path strings. It can extract the directory, the filename, the extension, and more.
The basic syntax is:
get_filename_component(<variable> <path> <component>)
Let's see it in action. Imagine we have a variable holding a full path to a file:
set(FULL_PATH "/path/to/my/file.txt")
We can extract different parts:
set(FULL_PATH "/path/to/my/file.txt")
get_filename_component(PARENT_DIR ${FULL_PATH} DIRECTORY)
# PARENT_DIR is now "/path/to/my"
get_filename_component(FILENAME ${FULL_PATH} NAME)
# FILENAME is now "file.txt"
get_filename_component(FILE_EXT ${FULL_PATH} EXT)
# FILE_EXT is now ".txt"
get_filename_component(NAME_NO_EXT ${FULL_PATH} NAME_WE)
# NAME_NO_EXT is now "file"
This is useful when you need to programmatically construct new paths based on existing ones, such as creating an output file with the same name as an input file but with a different extension.
The file()
Command
The file()
command is one of the most flexible commands in CMake. It's a single command that provides a huge collection of subcommands for different file-related tasks. The general syntax is:
file(<SUBCOMMAND> ...arguments...)
We can broadly categorize these subcommands into three groups:
- Reading: Getting content from files on disk.
- Writing: Creating new files or modifying existing ones.
- Filesystem Operations: Creating directories, copying files, and finding files that match a pattern.
Let's explore each of these categories.
Reading from Files
Sometimes, you want to store project information outside of your CMakeLists.txt
file. For example, you might have a simple text file containing your project's version number. The file()
command lets you read this data directly into a CMake variable.
file(READ)
This subcommand reads the entire content of a file into a single variable.
Let's create a version.txt
file in the root of our Greeter
project:
version.txt
1.2.3
Now, we can modify our root CMakeLists.txt
to read this version instead of hardcoding it. This pattern makes it easy to manage configuration data in separate, simple files that non-programmers can edit.
CMakeLists.txt
cmake_minimum_required(VERSION 3.16)
# Read the version.txt file
# Store it in avariable called GREETER_VERSION_FROM_FILE
file(READ version.txt GREETER_VERSION_FROM_FILE)
# Strip any excess white space from GREETER_VERSION_FROM_FILE,
# Then store the result in a variable called GREETER_VERSION
string(STRIP ${GREETER_VERSION_FROM_FILE} GREETER_VERSION)
message(STATUS "Configuring Version ${GREETER_VERSION}")
project(
Greeter
VERSION ${GREETER_VERSION}
HOMEPAGE_URL "https://www.studyplan.dev"
DESCRIPTION "A program that says hello to people"
)
# ...
-- Configuring Version 1.2.3
Our initial file()
command reads the content of version.txt
and stores it in the GREETER_VERSION_FROM_FILE
variable. This variable will likely contain additional white space - possibly a new line, such as "1.2.3\n"
.
string(STRIP ...)
is a handy utility command that removes leading and trailing whitespace. We use it to clean up the variable, so GREETER_VERSION
becomes just "1.2.3"
.
file(STRINGS)
While file(READ)
grabs the whole file, file(STRINGS)
is more specific: it reads a file and splits it into a CMake list, with each line of the file becoming an element in the list.
This is perfect for when you have a manifest file listing items, such as a list of source files. Let's imagine we have a file called sources.txt
in our app
directory:
app/sources.txt
src/main.cpp
src/tools.cpp
We can now read this into a list variable in app/CMakeLists.txt
:
app/CMakeLists.txt
cmake_minimum_required(VERSION 3.16)
file(STRINGS sources.txt SOURCES)
foreach(file IN LISTS SOURCES)
message(STATUS "Using file ${file}")
endforeach()
project(MyProject)
add_executable(MyTarget SOURCES)
-- Using file src/main.cpp
-- Using file src/tools.cpp
The SOURCES
variable will now be the list "src/main.cpp;src/goodbye.cpp"
, which is then correctly expanded when passed to add_executable()
. This approach can make it easier to manage long lists of files without cluttering your main CMakeLists.txt
.
Writing to Files
Just as we can read from files, we can also write to them. This is often used for generating files at configure time, such as a build information file or a configuration header.
file(WRITE)
and file(APPEND)
These commands are straightforward: WRITE
creates a new file (or overwrites an existing one), and APPEND
adds content to the end of a file.
Let's create a simple command that generates a build_info.txt
file containing the timestamp of when the project was configured. We'll use the string(TIMESTAMP)
command to get the current time.
CMakeLists.txt
cmake_minimum_required(VERSION 3.16)
project(MyProject VERSION "1.2.3")
string(TIMESTAMP BUILD_TIME "%Y-%m-%d %H:%M:%S UTC")
set(BUILD_INFO "Project: ${PROJECT_NAME}\n")
set(BUILD_INFO "${BUILD_INFO}Version: ${PROJECT_VERSION}\n")
set(BUILD_INFO "${BUILD_INFO}Configured at: ${BUILD_TIME}\n")
file(WRITE ${CMAKE_BINARY_DIR}/build_info.txt "${BUILD_INFO}")
message(STATUS "Wrote build info to ${CMAKE_BINARY_DIR}")
In this example, we're using the CMake-provided CMAKE_BINARY_DIR
variable. CMake's "binary directory" is what most developers call the "build directory" - that is, the directory from which we used our cmake
command.
So, in this case, when we configure our project, this will create a build_info.txt
file in our build
directory:
-- Wrote build info to D:/projects/cmake/build
build/build_info.txt
Project: MyProject
Version: 1.2.3
Configured at: 2025-07-29 23:19:09 UTC
An interesting use of this technique is to create C++ header files within CMake, and to then #include
those headers as needed in our source files.
This gives our program access to information that is normally only available within a CMakeLists.txt
script. We'll see a practical example of this later in the course.
Filesystem Operations
The file()
command also provides tools for manipulating the filesystem itself, such as creating directories and copying files.
Using file(GLOB)
and file(GLOB_RECURSE)
This is one of the most tempting, and most dangerous, subcommands in CMake. GLOB
allows you to find all files in a directory that match a certain pattern (a "globbing expression") and store them in a list.
For example, to find all .cpp
files in the src
directory, you could write:
file(GLOB GREETER_SOURCES "src/*.cpp")
add_executable(GreeterApp ${GREETER_SOURCES})
This looks incredibly convenient. You never have to manually update your CMakeLists.txt
when you add or remove a source file. However, this convenience comes at a high price.
Creating Directories
The file(MAKE_DIRECTORY)
subcommand ensures that one or more directories exist, creating them if they don't. It's useful in custom build steps where you need to guarantee an output directory is present before copying files into it.
# Ensure an assets directory exists in the build folder
file(MAKE_DIRECTORY ${CMAKE_BINARY_DIR}/assets)
Copying Files
The file(COPY)
subcommand copies files or entire directories from a source to a destination. It's a simple way to move resources needed at runtime into your build directory.
# Copy a config file into the build directory
file(COPY "data/config.json" DESTINATION ${CMAKE_BINARY_DIR}/)
Practical Example: Copying Assets
A very common use case for file operations is managing non-code assets that your application needs at runtime, such as images, configuration files, or fonts. You typically want to copy these assets from your source tree into your build directory so that they are available alongside your executable.
Let's extend our Greeter
project to handle this.
Step 1: Create the Asset Files
First, let's create an assets
directory in our project's root and add a couple of files. It doesn't matter what these files are or what they contain, we're just going to use them as placeholders to learn the process:
assets/
├─ logo.png
└─ config.json
CMakeLists.txt
Step 2: Update CMakeLists.txt to Copy the Files
Now, let's add logic to our root CMakeLists.txt
to copy these files into the build directory. We will use a combination of set()
, foreach()
, and file()
commands.
CMakeLists.txt
cmake_minimum_required(VERSION 3.16)
set(ASSET_FILES
logo.png
config.json
)
set(ASSET_DEST "${CMAKE_BINARY_DIR}/assets")
# Ensure the destination directory exists
file(MAKE_DIRECTORY ${ASSET_DEST})
foreach(asset IN LISTS ASSET_FILES)
# Build the full path to the current asset file
set(src_path "${CMAKE_CURRENT_SOURCE_DIR}/assets/${asset}")
# Copy the source file to the destination directory.
file(COPY ${src_path} DESTINATION ${ASSET_DEST})
message(STATUS "Copied ${src_path} to asset directory")
endforeach()
Step 3: Verify the Result
Now, when you run the configure step (cmake ..
from your build
directory), this script will execute.
cmake ..
-- Copied D:/Projects/cmake/assets/logo.png to asset directory
-- Copied D:/Projects/cmake/assets/config.json to asset directory
Afterward, your build directory will have a new, populated assets
folder, ready for your executable to use.
build/
├─ assets/
│ ├─ logo.png
│ └─ config.json
├─ ... (other generated files)
Summary
The file()
command is an important part of any non-trivial CMake script. It's your bridge between the abstract world of build targets and the concrete world of files and directories on disk.
- Reading: Use
file(READ)
to get a file's full content andfile(STRINGS)
to read it line-by-line into a list. - Writing: Use
file(WRITE)
andfile(APPEND)
to generate files at configure time. - Filesystem: Use
file(MAKE_DIRECTORY)
andfile(COPY)
for basic filesystem management. - Avoid
file(GLOB)
: Never usefile(GLOB)
to collect source files; it makes your build unreliable. Always list sources explicitly. - Path Management: Understand the key path variables (
CMAKE_SOURCE_DIR
,CMAKE_CURRENT_SOURCE_DIR
, etc.) and useget_filename_component()
for all your path manipulation needs.
Modularizing CMake Code
Learn to create reusable logic with CMake functions and macros, and how to organize them into modules for clean, scalable build systems.