1

I'd like my C or C++ program that is built via CMake to be able to print (or otherwise make use of) the macro definitions and (other) C/C++ flags it was compiled with. So I want CMake to generate/configure a header or source file that defines respective strings constants and that is then built as part of/into my program.

CMake features several commands (like file() or execute_process()) that would be executed when (respectively before) the build system is generated and thus would allow me to write such a source file, but I'm having trouble with getting the effective macro definitions and flags used for my target. E.g. there seem to be COMPILE_DEFINITIONS for the directory, the target, and for the configuration. Is there a way to get the macro definitions/C(++) flags that are effectively used for building my target? And how do I best write them into a source file?

I've noticed, when using the Makefiles generator apparently a file "${CMAKE_CURRENT_BINARY_DIR}/CMakeFiles/MyTarget.dir/flags.make" is created, which seems to contain pretty much what I'm looking for. So if there's no other way, I can probably make use of that file, but obviously that won't work for other generators and it comes with its own challenges (the file is generated after execute_process()).

1 Answer 1

0

The approach I finally went with sets the CXX_COMPILER_LAUNCHER property to use a compiler wrapper script that injects the actual compiler command line into a source file. Since I have multiple libraries/executables to which I want to add the respective information, I use a CMake function that adds a source file containing the info to the target.

function(create_module_build_info _target _module _module_include_dir)
    # generate BuildInfo.h and BuildInfo.cpp
    set (BUILD_MODULE ${_module})
    set (BUILD_MODULE_INCLUDE_DIR ${_module_include_dir})
    configure_file(${CMAKE_SOURCE_DIR}/BuildInfo.h.in
            ${CMAKE_BINARY_DIR}/include/${_module_include_dir}/BuildInfo.h
            @ONLY)
    configure_file(${CMAKE_SOURCE_DIR}/BuildInfo.cpp.in
            ${CMAKE_CURRENT_BINARY_DIR}/BuildInfo.cpp
            @ONLY)

    # Set our wrapper script as a compiler launcher for the target. For
    # BuildInfo.cpp we want to inject the build info.
    get_property(_launcher TARGET ${_target} PROPERTY CXX_COMPILER_LAUNCHER)
    set_property(TARGET ${_target} PROPERTY CXX_COMPILER_LAUNCHER
            ${CMAKE_SOURCE_DIR}/build_info_compiler_wrapper.sh ${_launcher})
    get_property(_compile_flags SOURCE BuildInfo.cpp PROPERTY COMPILE_FLAGS)
    set_property(SOURCE BuildInfo.cpp PROPERTY COMPILE_FLAGS
            "${_compile_flags} -D_BUILD_INFO=${CMAKE_CURRENT_BINARY_DIR}/BuildInfo_generated.cpp,${_module}")

    # add BuildInfo.cpp to target
    target_sources(${_target} PRIVATE BuildInfo.cpp)
endfunction()

The function can simply be called after defining the target. Parameters are the target, a name that is used as a prefix of the constant name to be generated, and a name that is part of the path of the header file to be generated. The compiler flag -D_BUILD_INFO=... is only added to the generated source file and it will be used by the wrapper script as an indicator that the constant definition should be added to that source file. All other compiler lines are just invoked as is by the script.

The template source file "BuildInfo.cpp.in":

#include "@BUILD_MODULE_INCLUDE_DIR@/BuildInfo.h"

The template header file "BuildInfo.h.in":

#pragma once

#include <string>

extern const std::string @BUILD_MODULE@_COMPILER_COMMAND_LINE;

The compiler wrapper script "build_info_compiler_wrapper.sh":

#!/usr/bin/env bash

set -e

function createBuildInfoTempFile()
{
    local source="$1"
    local target="$2"
    local prefix="$3"
    local commandLine="$4"
    cp "$source" "$target"
    cat >> "$target" <<EOF
const std::string ${prefix}_COMPILER_COMMAND_LINE = "$commandLine";
EOF
}

# Process script arguments. We copy them to array variable args. If we find an
# argument "-D_BUILD_INFO=*", we remove it and will inject the build info
# variable definition into (a copy of) the input file.

generateBuildInfo=false
buildInfoTempFile=
buildInfoVariablePrefix=
args=()

while [ $# -ge 1 ]; do
    case "$1" in
        -D_BUILD_INFO=*)
            if [[ ! "$1" =~ -D_BUILD_INFO=([^,]+),(.+) ]]; then
                echo "error: failed to get arguments for build info generation" >&2
                exit 1
            fi
            generateBuildInfo=true
            buildInfoTempFile="${BASH_REMATCH[1]}"
            buildInfoVariablePrefix="${BASH_REMATCH[2]}"
            shift
            continue
            ;;
    esac

    args+=("$1")
    shift
done

if $generateBuildInfo; then
    # We expect the last argument to be the source file. Check!
    case "${args[-1]}" in
        *.c|*.cxx|*.cpp|*.cc)
            createBuildInfoTempFile "${args[-1]}" "$buildInfoTempFile" "$buildInfoVariablePrefix" "${args[*]}"
            args[-1]="$buildInfoTempFile"
            ;;
        *)
            echo "error: Failed to find source file in compiler arguments for build info generation feature." >&2
            exit 1
            ;;
    esac
fi

"${args[@]}"

Obviously the script can be made smarter. E.g. instead of assuming it is the last argument it could find the actual index of the input source file. It could also process the command line to separate preprocessor definitions, include paths, and other flags.

Note that "-D_BUILD_INFO=..." argument is used instead of some parameter that the compiler wouldn't know (e.g. "--generate-build-info"), so that IDEs won't run into issues when passing the arguments directly to the compiler for whatever purposes.

Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.