Menu Share

How to build & run custom tools with CMake

by | Last Updated:
ANSI C, C++

This article will tackle the question how to build your own custom tools. Why would you need those custom tools? To make your life easy of course – you need that data directory copied relative to the built executable? You want to generate strongly typed classes based on your shader files? Want to compile your GLSL shaders to SPIR-V?

This is the solution of how to create custom tools in CMake!

Table of Contents

What you’ll need?

Before you proceed with reading this article I would expect that you have a decent knowledge in:

  • General C/C++ – If you want to start learning more about C++ you can visit some useful sites. You can also watch tutorials that teach you more on the subject.
  • CMake – Well you will also need to know what CMake is before you add your own tools to the build pipeline

What do we want to achieve?

In this article, I will write a somewhat simple program that will copy assets from a directory called data to the directory of the executable. The target goal is to do this step before or after our executable is built. Also, this should happen automatically every time you run “cmake –build”.

Project structure

Creating another C++ executable to serve as our custom tool

Note: I will be giving example with a C++ tool but you can also execute other applications on your system. Be careful though if you want to compile for multiple platforms to use tools that can be executed universally between those platfroms. If you’re interested only into how to run your own tools that are not C++ based check out only the next section.

As usual, I will start with an example project structure. You may have seen this in some of my other articles. We will have a project root with two executables – one will be used as a custom tool and will be built and executed before the other target can start. We will also have a data directory that would contain some example files. Keep in mind that I also use a library called ghc_filesystem by gulrak that implements the filesystem standard for multiple platforms because there are some differences based on the compiler version you might be using. It is included as a subdirectory so I will add it to the project structure also.

root
├── filesystem/
│ └── ...ghc_filesystem library...
├── Tools/
│ ├── src/
│ │ ├── Filesystem.h
│ │ ├── Tools.h
│ │ └── Tools.cpp
│ └── CMakeLists.txt
├── Main/
│ ├── src/
│ │ └── ...game files...
│ └── CMakeLists.txt
└── CMakeLists.txt

Keep in mind that I will not delve much into how Main CMakeLists.txt is setup at this moment or what it is compiling – it could be a library or an executable. We will just take for a given that it produces a cmake target with the same name as the directory.

Tools folder would also be quite simple:

project(Tools)

set(ToolsSources
    src/Tools.cpp
    src/Tools.h
    src/Filesystem.h
    )

add_executable(Tools
               ${ToolsSources}
               )

target_link_libraries(Tools PRIVATE ghc_filesystem)

target_include_directories(Tools
                           PRIVATE
                           ${CMAKE_CURRENT_SOURCE_DIR}/src
                           )

The filesystem header would also contain some useful functions for operating with the filesystem:

#ifndef PROJECT_FILESYSTEM_H
#define PROJECT_FILESYSTEM_H

#if (defined(_MSVC_LANG) && _MSVC_LANG >= 201703L) && defined(__has_include)
#if __has_include(<filesystem>) && (!defined(__MAC_OS_X_VERSION_MIN_REQUIRED) || __MAC_OS_X_VERSION_MIN_REQUIRED >= 101500)
#define GHC_USE_STD_FS

#include <filesystem>

namespace fs = std::filesystem;
#endif
#endif

#ifndef GHC_USE_STD_FS

#include <ghc/filesystem.hpp>

namespace fs = ghc::filesystem;
#endif

#endif //PROJECT_FILESYSTEM_H

Creating CMake custom targets

To be able to run our compiled Tools target we could do one of two things. Firstly add_custom_command (which must be executed in the same directory as our Main target). Secondly add_custom_target which is a bit more universal as we can run it from the root CMakeLists.txt file. Here is how the root list will look like after we add the command:

cmake_minimum_required(VERSION 3.16)
project(Project)

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

add_subdirectory(filesystem)
add_subdirectory(Tools)
add_subdirectory(Main)

add_custom_target(ToolsRun
                  COMMAND Tools "${CMAKE_CURRENT_SOURCE_DIR}" "${Main_BINARY_DIR}"
                  WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
                  COMMENT "Executing data generation app"
                  SOURCES ${ToolsSources}
                  )

add_dependencies(Main ToolsRun)

You can see that except the subdirectory additions we only have the custom target defined. If you read the official documentation this custom target can run any command so if you have a python script that you want to execute you could simply invoke python to execute that script.

Like adding a library you must define your target name and we name it ToolsRun. On the next command you can see that we also set Main to depend on ToolsRun which will force cmake to compile them in that order.

If we take a closer look at the arguments given to add_custom_target you will see that we define the COMMAND to be executed. This command will be executed in the native terminal emulator for the system. In the C++ example we pass the name of the target that we would like to run. This will instruct cmake to build that target first and as part of the build process of Main also execute the target.

You will notice that we also pass some command line arguments. This is because we want to know what is the root directory of the project and where to copy the files in the end.

We also set the working directory to be the current directory (the root of our project). The comment is what will be shown in the console during build when the target is executed. The SOURCES will just add the sources if you’re using an IDE to the separate target.

Writing the code to copy the directory and all its contents

I will also add a listing of what can be an example code for copying the data contents:

#include "Filesystem.h"
#include "Tools.h"

// Looks up a directory and fills a list with all the filenames
void EnumerateFiles(const std::string& path, std::vector<std::string>& files)
{
	for (auto& content : fs::directory_iterator(path))
	{
		if (fs::is_regular_file(content.path().string()))
		{
			files.push_back(content.path().filename().string());
		}
	}
}


void CopyDirectoryContents(const std::string& src, const std::string& dest)
{
        // We want to create the same directory in destination
	if (!fs::exists(dest))
	{
		fs::create_directory(dest);
	}
	
        // We take the source files
	std::vector<std::string> srcFiles;
	EnumerateFiles(src, srcFiles);
	
        // And we copy each one of them
	for (const std::string& path : srcFiles)
	{
		fs::copy_file(src + "/" + path, dest + "/" + path, fs::copy_options::overwrite_existing);
	}
}

int main(int argc, char** argv)
{
	if (argc != 3)
	{		
		return 1;
	}
	
	CopyDirectoryContents(std::string(argv[1]) + "/data", std::string(argv[2]) + "/data");
	
	return 0;
}

This is an example of one pretty simple tool that you can write to copy your data files. You can go very complicated with this and create tools that would generate header files used in other targets. Like a program that reads and parses shaders and creates some strongly typed classes for the main engine application.

Some ideas/challenges that you can take for improvement of this simple tool could be:

  • Delete files in the destination directory that are no longer in the source directory
  • Copy directories recursively
  • Only update files that have been modifies (are more recent)

Conclusion

CMake is quite powerful for your projects. It may seem complex but it lifts a lot off your shoulders.

Leave a comment