CMake configure_file: Embedding JSON in C++ file at build step

Introduction

This post explains how to use configure_file() in CMake to embed JSON content into a C++ header. In my first try I highlight the limitation in detecting JSON changes at the build step. In the second try I improve it using a custom command to update the file at the build step. file(GENERATE) is also discussed but found less user friendly for this use case.

configure_file

With configure_file(), we can create a template for a C++ header, implementation file, or any other project file. The template contains variables in the special formats @MyVar@, ${MyVar}, or $CACHE{MyVar}, which are replaced by CMake during the configure step. Environment variables can also be captured using $ENV{VAR}.

Problem

The problem that I aim to solve is to read a JSON file and embed its content into a header file as a string variable.

The template file people.h.in, is:

//people.h.in
#pragma once
#include <string>

std::string s = R"(@JSON_CONTENT@)";

The @JSON_CONTENT@ needs to be replaced by content of a json file, people.json:

{
    "name": "Neo",
    "lastname": "Anderson",
    "job": "programmer"
}

The outcome header, people.h, is included in the code below. The main function prints the string, i.e JSON_CONTENT:

//main.cpp
#include<iostream>
#include "people.h"

int main(){

    std::cout<< s <<"\n";
}

First try

My first try is to read the JSON file and store it in JSON_CONTENT variable. Then configure_file() replaces @JSON_CONTENT@ in the header file with what is in the variable:

cmake_minimum_required(VERSION 3.23)
project(MyExample)

# Read the JSON file
file(READ ${CMAKE_SOURCE_DIR}/people.json JSON_CONTENT)

# Configure the header file
configure_file(${CMAKE_SOURCE_DIR}/people.h.in ${CMAKE_BINARY_DIR}/people.h @ONLY)

add_executable(MyExample main.cpp)

# people.h is in the binary directory, let's include it
target_include_directories(MyExample PRIVATE ${CMAKE_BINARY_DIR})

This works fine, but I noticed that after a successful build, editing the people.json file does not trigger an update to people.h. This happens because configure_file() runs only during the configure step, not the build step. Unfortunately, this is inconvenient in Visual Studio, as reconfiguring tends to restart the solution.

Second try

In my second attempt, I move the configure_file() part to a separate script and call it with a custom command. We know that commands are run at the build step. See the files:

# CMakeLists.txt
cmake_minimum_required(VERSION 3.23)
project(MyExample)

set(My_OUTPUT_FILE "${CMAKE_BINARY_DIR}/people.h")

add_custom_command(
  OUTPUT ${My_OUTPUT_FILE}
  COMMAND ${CMAKE_COMMAND} -D projectDir=${PROJECT_SOURCE_DIR} -D binaryDir=${PROJECT_BINARY_DIR} -P ${CMAKE_SOURCE_DIR}/updatePeopleHeader.cmake 
  DEPENDS
    ${CMAKE_CURRENT_SOURCE_DIR}/people.json
  VERBATIM)

add_custom_target(runCommand ALL DEPENDS ${My_OUTPUT_FILE})

add_executable(MyExample main.cpp)
target_include_directories(MyExample PRIVATE ${CMAKE_BINARY_DIR})

The script is:

# updatePeopleHeader.cmake file

# Read the JSON file
file(READ ${projectDir}/people.json JSON_CONTENT)

# Configure the header file
configure_file(${projectDir}/people.h.in ${binaryDir}/people.h @ONLY)

message("++ people.h file is updated as people.json changed.")

Now whenever people.json is edited, the custom command is run to update people.h. To learn how a custom command works, see this post.

How about file(Generate)

In a post on generator expressions I mentioned that file(Generate) is a good way to debug generator expressions. See this example:

file(GENERATE OUTPUT ${CMAKE_SOURCE_DIR}/log.txt 
       CONTENT "compiler id: $<CXX_COMPILER_ID>, compiler version:$<CXX_COMPILER_VERSION>" 
       TARGET circle)

In an example project, it made below output:

compiler id: MSVC, compiler version:19.42.34436.0

One can write an output file and let CMake replace generator expressions with what what is suitable. However, file(Generate) still works at the generate step of CMake process, and not the build step. Moreover, working with generator expressions is not as clean as configure_file() variables. Therefore, the second try with configure_fie() is the best way to modify files at the build step.

Tags ➡ C++ CMake

Subscribe

I notify you of my new posts

Latest Posts

Comments

0 comment