CODE: math(OUTPUT_FORMAT HEXADECIMAL) workaround for CMake < 3.13

One of our CMake projects includes a GENERATED config file that needs to include the project version converted into hexadecimal form. With CMake 3.13+, easy-peasy:

  math(EXPR PROJECT_VERSION_HEX
    "(${PROJECT_VERSION_MAJOR} << 16) + \
     (${PROJECT_VERSION_MINOR} << 8) + \
     (${PROJECT_VERSION_PATCH})" OUTPUT_FORMAT HEXADECIMAL )

But we also need to support CMake versions all the way down to 3.2 (I knoooow), where that won’t work. For the longest time we were just hardcoding “0x0m0n0p” in a variable that we tried (and failed) to remember to update whenever ${PROJECT_VERSION} changed.

Since that was tedious even when we did remember (and we usually didn’t), I eventually hit on a workaround that I wanted to share in case it’s useful to others.

The trick is to leverage the CMake try_run() functionality, which can be used to execute a C or C++ program during the build generation phase — much like execute_process(), but for source code instead. (You could certainly also do this with a shell command and execute_process().)

I chose to write my hex_version.cpp generically, in case it ever has other uses. So, the inputs are passed as COMPILE_DEFINITIONS on the try_run() command line, and it could theoretically be used multiple times with different arguments. Source:

#include <iostream>
#include <ios>

// The following values must be defined at compile time:
// VERSION_MAJOR, VERSION_MINOR, VERSION_PATCH

#if !defined(VERSION_MAJOR) || !defined(VERSION_MINOR) || !defined(VERSION_PATCH)
    #pragma error "Define version components on compiler command line!"
#endif

int main()
{

    int hex_version = (VERSION_MAJOR << 16) +
                      (VERSION_MINOR << 8) +
                      (VERSION_PATCH);

    std::cout << std::hex << "0x" << hex_version;
}

Then, when older CMake versions process CMakeLists.txt, that’s called to retrieve the PROJECT_VERSION_HEX value instead of using math():

# Compile and run a C++ program to generate the hex version
set(HEX_COMPILE_DEFINITIONS
  -DVERSION_MAJOR=${PROJECT_VERSION_MAJOR}
  -DVERSION_MINOR=${PROJECT_VERSION_MINOR}
  -DVERSION_PATCH=${PROJECT_VERSION_PATCH}
)
try_run(HEX_VERSION_RUN HEX_VERSION_BUILD
  ${CMAKE_CURRENT_BINARY_DIR}/hex_version
  ${PROJECT_SOURCE_DIR}/src/hex_version.cpp
  COMPILE_DEFINITIONS ${HEX_COMPILE_DEFINITIONS}
  RUN_OUTPUT_VARIABLE HEX_VERSION_OUTPUT
)
if (NOT HEX_VERSION_BUILD)
  message(ERROR "Failed to compile hex-version utility!")
elseif(HEX_VERSION_RUN STREQUAL FAILED_TO_RUN)
  message(ERROR "Could not execute hex-version utility!")
else()
  set(PROJECT_VERSION_HEX ${HEX_VERSION_OUTPUT})
endif()
1 Like

You could divide the number by 16 and use if and elseif to check for and convert the 6 possibilities greater 10 and simply assemble the string this way.

I’ve done something similar in a project of mine:

function(_FRUT_dec_to_hex dec_value out_hex_value)

  if(dec_value EQUAL 0)
    set(${out_hex_value} "0x0" PARENT_SCOPE)
    return()
  endif()

  if(dec_value LESS 0)
    math(EXPR dec_value "2147483647 ${dec_value} + 1")
  endif()

  while(dec_value GREATER 0)
    math(EXPR hex_unit "${dec_value} & 15")
    if(hex_unit LESS 10)
      set(hex_char "${hex_unit}")
    else()
      math(EXPR hex_unit "${hex_unit} + 87")
      string(ASCII ${hex_unit} hex_char)
    endif()
    set(hex_value "${hex_char}${hex_value}")
    math(EXPR dec_value "${dec_value} >> 4")
  endwhile()

  set(${out_hex_value} "0x${hex_value}" PARENT_SCOPE)

endfunction()

(from FRUT/cmake/Reprojucer.cmake at f89e16465abc4a7eb141459e48e718f2c866b4bb · McMartin/FRUT · GitHub)

1 Like

OK, the use of string(ASCII) to convert decimal to hexadecimal is legitimately inspired.