What can I replace "INSTALL" with?

Hello, I have a CMakeLists.txt that I’m not entirely happy with as a user of Visual Studio 2022.

Basically, I haven’t found a solution to the catch-22 that is: if i do build ALL_BUILD the INSTALL step happens and the application is successfully built, but this configuration cannot be debugged directly.

If I instead change to build MyApp, the INSTALL step will never execute and thus the debugger cannot find the files.

I do not like having to first do the ALL_BUILD, switch to MyApp and then run the debugger.
What are some options to patch the CMakeLists.txt file?

Relevant code:


# Set the correct debugger working directory in case Visual Studio is to be used
set_property(TARGET ${PROJECT_NAME} PROPERTY VS_DEBUGGER_WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}/bin")

# Without the trailing / after assets, it would copy the directory instead of its contents
install(TARGETS ${PROJECT_NAME} DESTINATION ${CMAKE_SOURCE_DIR}/bin) #executable

install(DIRECTORY "$<TARGET_FILE:tgui>" DESTINATION ${CMAKE_SOURCE_DIR}/bin) # libtgui.a

install(DIRECTORY ${PROJECT_SOURCE_DIR}/src/assets/ DESTINATION ${CMAKE_SOURCE_DIR}/bin) # logo.png and other assets

install(DIRECTORY ../build/_deps/tgui-src/themes/ DESTINATION ${CMAKE_SOURCE_DIR}/bin/themes) # black.txt and etc theme-specific files

If you just want to create a runnable/debuggable copy of your application after it’s compiled, I definitely wouldn’t recommend using install() for that.

I also wouldn’t recommend writing any destination paths relative to ${CMAKE_SOURCE_DIR}, under any circumstances. Install paths are meant to be relative to the ${CMAKE_INSTALL_PREFIX} (but that’s implicit, you don’t have to write it), and I think that defaults to C:\Program Files\ on Windows — but it can be changed at install time. And any other paths you write to during the build process should be relative to the ${CMAKE_BINARY_DIR}, instead of polluting your source tree.

It’s common to consolidate application files, necessary DLLs, etc. into a directory inside the build directory, when running on Windows, because those are often necessary prerequisites for running the program uninstalled. (Or running tests on the compiled programs/DLLs.)

The way that’s typically done is by using CMake’s -E mode to run common filesystem commands (you can run cmake -E help to get a list of them); you’d execute those commands inside an execute_process() (for static files that are available at configure time), or as an add_custom_command(TARGET <name> POST_BUILD) step that will run after the target’s build step is complete.

So, you might do something like this:

add_custom_command(TARGET ${PROJECT_NAME} POST_BUILD
  COMMAND
    ${CMAKE_COMMAND} -E make_directory
      "${CMAKE_BINARY_DIR}/bin"
  COMMAND 
    ${CMAKE_COMMAND} -E copy
      "$<TARGET_FILE:${PROJECT_NAME}>"
      "${CMAKE_BINARY_DIR}/bin/"
)

execute_process(
  COMMAND
    ${CMAKE_COMMAND} -E make_directory
      "${CMAKE_BINARY_DIR}/bin"
  COMMAND
    ${CMAKE_COMMAND} -E copy_directory
      "${PROJECT_SOURCE_DIR}/src/assets"
      "${CMAKE_BINARY_DIR}/bin/"
)

And so on for anything else. The make_directory will silently succeed if the dir already exists, so it’s safest to do it redundantly.

Any execute_process() commands will execute during the configure step, so the files they’re accessing have to already exist. For files that only exist after certain targets are built, use add_custom_command() to set the copy up as a POST_BUILD step.

(That includes anything that comes from tgui-src, as that won’t exist when cmake is setting up the initial config. Tho presuming that’s a FetchContent package, you should use the variable ${tgui_SOURCE_DIR} to reference paths in that location. FetchContent_Populate() will create that variable when it’s run.)

Edit: Note that all of these commands should ideally be used in addition to a set of install() commands that make your program actually installable. But like I said, those commands should use paths that are implicitly relative to the ${CMAKE_INSTALL_PREFIX}, which on Windows often means installing everything to a directory named after the project, or maybe "${PROJECT_NAME}-${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR}":

set(PROJECT_INSTALL_DIR
 "${PROJECT_NAME}-${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR}")
install(TARGETS ${PROJECT_NAME}
  RUNTIME DESTINATION ${PROJECT_INSTALL_DIR}
  LIBRARY DESTINATION ${PROJECT_INSTALL_DIR}
  ARCHIVE DESTINATION ${PROJECT_INSTALL_DIR}
)
install(DIRECTORY
  "${CMAKE_SOURCE_DIR}/src/assets/"
  DESTINATION ${PROJECT_INSTALL_DIR}
)
install(DIRECTORY
  "${tgui_SOURCE_DIR}/themes"
  DESTINATION ${PROJECT_INSTALL_DIR}
)

I believe it’s safe to use ${tgui_SOURCE_DIR} in the install() command, as long as that variable has already been set, because the install() command just generates commands to a cmake_install.cmake script that gets executed to perform the install. And at install time, you know the tgui source will already have been downloaded.

It technically isn’t doing that “pollution” afaik. Because i have a root directory with CMakeLists.txt which calls CMakeLists.txt inside “src” folder (this one holds the actual build process, including fetching deps)
Reference: GitHub - Snowblaze/sdl2-cmake-starter (which i have edited a fair bit)

Edit: I recall messing around with the ${CMAKE_BINARY_DIR} but never actually getting it to “stick” with Visual Studio, it always ended up in a weird path after a while. This makes the built application annoying to package, especially since it would leave me with random duplicates.

It absolutely is. ${CMAKE_SOURCE_DIR} is the source directory of your project, the place your top-level CMakeLists.txt is located. ${CMAKE_CURRENT_SOURCE_DIRECTORY} is the directory of the current CMakeLists.txt file being processed. But they’re all source directories. If you create anything during the build, it should be somewhere inside ${CMAKE_BINARY_DIR} or ${CMAKE_CURRENT_BINARY_DIR}.

I know you’re using Visual Studio, which probably has its own weird ideas about where to store build artifacts (Qt Creator does as well), but that’s a tool issue. If you were to go into a terminal session and run this:

$ cd c:\path\to\your\toplevel\source\directory
$ cmake -B build -S . -G "Visual Studio 17 2022"

What would happen is that CMake would set up a directory C:\path\to\your\toplevel\source\directory\build where all of the generated configuration and build artifacts live.

When it processes the top-level CMakeLists.txt, both ${CMAKE_BINARY_DIR} and ${CMAKE_CURRENT_BINARY_DIR} are set to C:\path\to\your\toplevel\source\directory\build. Both ${CMAKE_SOURCE_DIR} and ${CMAKE_CURRENT_SOURCE_DIR} are set to C:\path\to\your\toplevel\source\directory.

Then, when it descends into the src directory due to the add_directory(src) in the toplevel CMakeLists.txt, ${CMAKE_SOURCE_DIR} and ${CMAKE_BINARY_DIR} stay the same, but ${CMAKE_CURRENT_SOURCE_DIR} becomes C:\path\to\your\toplevel\source\directory\src, and ${CMAKE_CURRENT_BINARY_DIR} becomes C:\path\to\your\toplevel\source\directory\build\src.

But if you write anything into any _SOURCE_DIR path, you’re storing it somewhere in the source of your project.

Install destinations

Writing into the source dir can be valid, for install artifacts — you may want to install your program to C:\path\to\your\toplevel\source\directory\bin (a path outside the build directory). But you should do that by setting the CMAKE_INSTALL_PREFIX to C:\path\to\your\toplevel\source\directory\bin (by adding --install-prefix C:\path\to\your\toplevel\source\directory\bin or -DCMAKE_INSTALL_PREFIX=C:\path\to\your\toplevel\source\directory\bin to the cmake command / VS CMake configuration, not by hardcoding it in your CMakeLists.txt file), then installing the artifacts right in the install directory:

install(TARGETS ${PROJECT_NAME}
  RUNTIME DESTINATION "."
  LIBRARY DESTINATION "."
  ARCHIVE DESTINATION "."
)
install(DIRECTORY
  "${CMAKE_SOURCE_DIR}/src/assets/"
  DESTINATION "."
)
# This will create "${CMAKE_INSTALL_PREFIX}/themes"
# with the contents of the directory
install(DIRECTORY
  "${tgui_SOURCE_DIR}/themes" DESTINATION "."
)

But the uninstalled, debuggable files that CMake creates during the build should be inside the build directory, so that they can be deleted with the build directory, and so that they’ll be recreated with each build, not persist outside the space where it’s unclear which build they’re from or how they were compiled. (In fact, if you add a BYPRODUCTS argument to add_custom_command() listing the files created, they’ll be marked as additional build artifacts and deleted when you clean the build directory.)

add_custom_command(TARGET ${PROJECT_NAME} POST_BUILD
  COMMAND
    ${CMAKE_COMMAND} -E make_directory
      "${CMAKE_BINARY_DIR}/bin"
  COMMAND 
    ${CMAKE_COMMAND} -E copy
      "$<TARGET_FILE:${PROJECT_NAME}>"
      "${CMAKE_BINARY_DIR}/bin/"
  BYPRODUCTS
    "${CMAKE_BINARY_DIR}/bin/$<TARGET_FILE_NAME:${PROJECT_NAME}>"
)

There’s probably no real reason to do that for the files created with execute_process(), because they’re static and don’t need to be recreated. But if you think you might be editing those and want them to be recreated during the build, you can do that copying in add_custom_command() calls with BYPRODUCTS as well.

Taking a step back

I should’ve mentioned that if your goal is simply to make the ${PROJECT_NAME} target output debuggable, and it needs additional files in its directory to do so, it might be better/simpler not to copy that file itself, and instead just copy the other files needed into the same directory where it’s built.

In the same CMakeLists.txt where you create the ${PROJECT_NAME} target with add_library() / add_executable(), you can copy in all of the required files to that same directory as a post-build step:

add_custom_command(TARGET ${PROJECT_NAME} POST_BUILD
  COMMAND 
    ${CMAKE_COMMAND} -E copy_directory
      "${CMAKE_CURRENT_SOURCE_DIR/assets/"
      "${CMAKE_CURRENT_BINARY_DIR}"
  COMMAND
    ${CMAKE_COMMAND} -E copy_directory
      "${tgui_SOURCE_DIR}/src/themes"
      "${CMAKE_CURRENT_BINARY_DIR}/themes"
  BYPRODUCTS
    # (List the files copied from 
    # "${CMAKE_CURRENT_SOURCE_DIR}/assets/")
    "${CMAKE_CURRENT_BINARY_DIR}/themes"
)

…If those files are all that’s required, that should make $<TARGET_FILE:${PROJECT_NAME}> (aka ${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_NAME}.exe probably) executable directly from its build location, which is the simplest way to make it debuggable, and avoids making a redundant extra copy of the executable.

I thought the structure of the reference project was to make this “not the case”, but okay.

Moving on:
I cannot trust the BINARY_DIR due to inconsistent behavior in VS2022 - that’s why i want to consolidate everything into a separate directory. (This helps setting up the debugger as well as packaging)

Because I have tried to redirect where VS puts the built files and it is extremely “moody” for a lack of a better word. Sometimes it follows the directions given, sometimes it just ignores it. (this is the duplicates I meant)

Can you elaborate on the issues you have with VS regarding build directory?

From my experience, it is working just fine.

It reverted to building in weird paths, despite not changing anything but rewriting a couple of .hpp files and nothing I did would convince it to reliably use the supplied redirect anymore

I wasted 2 weeks off and on getting nowhere back in March/April, then got tired of the problem and so I changed from trying to make VS2022 “behave” to “let it do it’s thing” and just consolidate files elsewhere, which has been working fine, apart from the issues described in the post at the top.

Something about redirecting CMAKE_BINARY_OUTPUT_PATH is wonky when it comes to VS2022 and I haven’t found a pattern to this. CMAKE_CURRENT_BINARY_DIR may also be affected, at this point, I don’t know more.

When the redirect is not respected, the executable ends up in like: MyApp/build/src/<cpu architecture>/<build type>/

Now i’m not opposed to building release/debug in different folders, but it is not necessary for this project i’m working on. (also that output path is weird, it shouldn’t have src in it, but rather ${PROJECT_NAME} i believe? )