Working with the File System

Exploring CMake's file() command, covering how to read, write, copy, and glob files, and how to manipulate paths.

Greg Filak
Published

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 root CMakeLists.txt is). These variables are often the same, but there is a subtle difference within builds that contain multiple project() 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 the cmake command. Again, they are usually the same, but there is a subtle difference when we have multiple project() commands.
  • CMAKE_CURRENT_SOURCE_DIR: The absolute path to the source directory containing the CMakeLists.txt file that is currently being processed. This will be different from CMAKE_SOURCE_DIR when it is used in a CMakeLists.txt file that was loaded through the add_subdirectory() command.
  • CMAKE_CURRENT_BINARY_DIR: The absolute path to the build directory corresponding to the current source directory. Again, this will be different to CMAKE_BINARY_DIR within the add_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 and file(STRINGS) to read it line-by-line into a list.
  • Writing: Use file(WRITE) and file(APPEND) to generate files at configure time.
  • Filesystem: Use file(MAKE_DIRECTORY) and file(COPY) for basic filesystem management.
  • Avoid file(GLOB): Never use file(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 use get_filename_component() for all your path manipulation needs.
Next Lesson
Lesson 18 of 18

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.

Have a question about this lesson?
Answers are generated by AI models and may not be accurate