Configuring a file that has both '@' substitution and generator expressions

So, I was trying to do a script to pass to install(SCRIPT ...) to package an application.

For clarity of where my problem comes from, I was trying to run windeployqt.exe [...] myapp.exe from the script.

This is what I wrote:
This file is DeployOnWindows.cmake[.in]:

execute_process(
  COMMAND @WINDEPLOYQT_EXECUTABLE@ --dry-run --no-compiler-runtime --list
          mapping $<TARGET_FILE:app>
  OUTPUT_VARIABLE _output
  OUTPUT_STRIP_TRAILING_WHITESPACE)

# [...]

And in the main CMakeLists.txt I have something like:

# [...]
add_executable(app src/main.cpp)
target_link_libraries(app Qt5::Widgets)
# [...]
install(TARGETS app)

find_program(
  WINDEPLOYQT_EXECUTABLE
  NAMES windeployqt
  HINTS ${QTDIR} ENV QTDIR
  PATH_SUFFIXES bin)

# And this is what I want to do:
 # Maybe "${CMAKE_BINARY_DIR}/DeployOnWindows.cmake" after configuration from a DeployOnWindows.cmake.in...
install(SCRIPT "DeployOnWindows.cmake")

include(CPack)

Now, the problem: notice that my eventual .in file has both an @ variable, and a generator expression.
I started looking at my options:

  • configure_file does not deal with generator expressions.
  • file(GENERATE OUTPUT [...] INPUT [...]) does deal with generator expressions but not with @ variable substitution.
    • Very interestingly, and “New in version 3.18”, file(GENERATE OUTPUT [...] CONTENT [...]) does work with both but the INPUT version doesn’t.

I tried this, out of desperation:

# This handles generator expressions
file(
  GENERATE
  OUTPUT "${CMAKE_BINARY_DIR}/DeployOnWindows.cmake.in"
  INPUT "${CMAKE_SOURCE_DIR}/DeployOnWindows.cmake.in")
# This handles variable names
configure_file("${CMAKE_BINARY_DIR}/DeployOnWindows.cmake.in"
               "${CMAKE_BINARY_DIR}/DeployOnWindows.cmake" @ONLY)

But configure_file does not find the file and errors out the generation. But it does work after generating again. The file on the CMAKE_BINARY_DIR from file(GENERATE ... does get created, but it seems as if only after CMake stops. I don’t understand it.

So, are there suggestions to what I can do to achieve this?

Generation step is done after configuration step.
So, you have to execute your two commands in the reverse order:

configure_file("${CMAKE_SOURCE_DIR}/DeployOnWindows.cmake.in"
               "${CMAKE_BINARY_DIR}/DeployOnWindows.cmake.in" @ONLY)

file(
  GENERATE
  OUTPUT "${CMAKE_BINARY_DIR}/DeployOnWindows.cmake"
  INPUT "${CMAKE_BINARY_DIR}/DeployOnWindows.cmake.in")
1 Like

The file(GENERATE) command is a bit unusual in that it doesn’t write out the file immediately as part of the call, the file is only written during the generation phase, which is after CMake has read and processed the entire project. Therefore, in your last code block where you call configure_file() after file(GENERATE), the input file that configure_file() expects won’t have been written yet.

You can switch the order to get the behavior you want:

# This handles variable names
configure_file(DeployOnWindows.cmake.in DeployOnWindows.cmake.in @ONLY)

# This handles generator expressions (absolute paths to avoid policy CMP0070 issues)
file(
  GENERATE
  OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/DeployOnWindows.cmake
  INPUT ${CMAKE_CURRENT_BINARY_DIR}/DeployOnWindows.cmake.in
)

Some key points for the above:

  • When relative paths are given to the configure_file() command, the source is assumed relative to CMAKE_CURRENT_SOURCE_DIR and the destination is assumed relative to CMAKE_CURRENT_BINARY_DIR. I’ve taken advantage of that to simplify the call.
  • The file(GENERATE) command only has well-defined behavior for CMake 3.10 if relative paths are used. I’ve used absolute paths here to avoid that in case you need to support earlier CMake versions.
  • Both configure_file() and file(GENERATE) will leave the destination file’s timestamp untouched if the file already exists and has the required contents. If something in the build had a dependency on the final DeployOnWindows.cmake file, this would be important, since it would prevent unnecessary rebuilds. It doesn’t matter in your scenario, but I’m mentioning it because it may be relevant for others and it is a point of difference with the other method I’ve shown further below.

The above example writes out two different files to the build directory. If you wanted to avoid writing out the intermediate file, you could do it this way instead:

file(READ ${CMAKE_CURRENT_SOURCE_DIR}/DeployOnWindows.cmake.in contents)
string(CONFIGURE "${contents}" contents @ONLY)
file(GENERATE
    OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/DeployOnWindows.cmake
    CONTENT "${contents}"
)

The above always writes to the destination file and therefore always updates its timestamp. If that mattered, you could first read in the destination file if it exists and compare that to the contents you want to write and then only overwrite the existing file if the contents don’t match (or if there is no existing destination file). It’s a few more lines, but achieves the same result as the first example above without writing any intermediate file. In some scenarios, the performance hit of file system interactions on some platforms matters, but I doubt it does here for you, so I’d go with the first approach for simplicity.

Thanks for the answer. I guess since file(GENERATE ... OUTPUT ... CONTENT ...) does process both variables and generator expressions, it would be possible to do just:

file(READ ${CMAKE_CURRENT_SOURCE_DIR}/DeployOnWindows.cmake.in contents)
file(GENERATE
    OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/DeployOnWindows.cmake
    CONTENT "${contents}"
)

no?

Also, I still think this should have been possible with just file(GENERATE OUTPUT [...] INPUT [...]), and that’s why I raised the issue before.

Anyhow, I get it now, thanks!

What do you mean by that? file(GENERATE) will replace neither ${SOME_VAR} nor @SOME_VAR@. Perhaps you are thinking of file(CONFIGURE) instead?

Yes, yes I am. I misread the documentation. My bad.