Best practice for coexisting release/debug installations

What’s the best general practice for creating installation where Release and Debug binaries can coexist?

Simplest way would be to have them in separate folders,
but that requires changing path prefix/roots when you want to switch between Debug and Release builds.

Another option is to install both builds into the same folder - which works when debug binaries
have either d or _d suffixes, but that is not always the case.

There’s a vcpkg-way - vcpkg installs Relase and Debug to separate folders - e.g. install and install/debugand then is moving cmake configs from debug/lib/cmake/pkg (or whatever name package has) to debug/lib/cmake/pkg and patches them to make sure they work correcrly from the new location (script for reference). But that requires vcpkg environment and package to be vcpkg port.

Inspired by vcpkg approach - I came up with another one (example powershell script is below):

  • installing Release build to install
  • installing Debug to install/debug, but installing debug binaries to debug/debug
  • move cmake files from install/debug/lib/cmake/pkg to install/lib/cmake/pkg - there’s no need to patch them, since we prefixed our debug binaries with another debug folder.
  • move all files from debug/debug to debug

So now in install/lib/cmake/pkg we have pkgTargets-debug.cmake and mypkgTargets-release.cmake pointing to existing binaries, so should be all good.

Any idea if there are caveats to this approach or it should indeed work for the most cases?

$INSTALLATION_PATH="L:/install"
$CMAKE_CONFIG_LOCATION="lib/cmake/mypkg"
cmake .. -G Ninja `
    -DCMAKE_INSTALL_PREFIX="$INSTALLATION_PATH" `
    -DCMAKE_BUILD_TYPE=Release
cmake --build . --target install

cmake .. -G Ninja `
    -DCMAKE_INSTALL_PREFIX="$INSTALLATION_PATH/debug" `
    -DCMAKE_BUILD_TYPE=Debug `
    -DCMAKE_INSTALL_LIBDIR="debug/lib" `
    -DCMAKE_INSTALL_BINDIR="debug/bin"
cmake --build . --target install

cp -Verbose -Recurse "$INSTALLATION_PATH/debug/$CMAKE_CONFIG_LOCATION/*" "$INSTALLATION_PATH/$CMAKE_CONFIG_LOCATION"
rm -Verbose -Recurse "$INSTALLATION_PATH/debug/$CMAKE_CONFIG_LOCATION"
rm -Verbose -Recurse "$INSTALLATION_PATH/debug/lib"
rm -Verbose -Recurse "$INSTALLATION_PATH/debug/include"
cp -Verbose -Recurse "$INSTALLATION_PATH/debug/debug/*" "$INSTALLATION_PATH/debug"
rm -Verbose -Recurse "$INSTALLATION_PATH/debug/debug"

Whichever path you take, please ensure that things don’t get moved after a cmake --install, or whatever method you use to install them. If you must move things, only move them as an entire unit, don’t move only part of the install. Package authors shouldn’t have to deal with someone putting things in different (relative) places to what they specified when they wrote their install rules. I say this as someone who has had to deal with that specific behavior in vcpkg with consulting clients in the past, and it was very much a sore point.

If you are asking this from the point of view of only how to install your own package/project, the best approach may depend on what your project installs. If it is only installing libraries and not executables, then a co-located structure would probably be the more canonical. Libraries would have config-specific suffixes and be in the same directory. Ideally, headers wouldn’t be config-specific. An API that changes depending on the build configuration is inviting trouble. Having functions that are only implemented for certain build configurations is reasonable though, and this is not all that unusual for functions only available in Debug builds.

To illustrate at least one scenario where the above strategy helps, consider building a project with a multi-config generator like Ninja Multi-Config. You only configure it once, but build and install multiple configurations. That means the CMAKE_INSTALL_PREFIX must be the same for all build configurations. The workflow might go something like this:

cmake -G "Ninja Multi-Config ` -S . -B build
cmake --build build --config Debug
cmake --build build --config Release
cmake --install build --prefix /path/to/staging/area --config Debug
cmake --install build --prefix /path/to/staging/area --config Release

I recommend you use cmake --install rather than building the install target, as the target-based method doesn’t allow you to specify any options like the configuration to install. Note also how it allows you to specify where to install to at install time, you don’t have to hard-code something at configure time (i.e. no need to set CMAKE_INSTALL_PREFIX at all).

The one caveat with the above is installing executables. It is unusual to put config-specific suffixes on executables. But it is also unusual to install anything other than Release executables. You are not linking to executables, so why would you need a Debug version installed? It should be enough to have only the Release executable. If you look at the example I gave above, you’ll see that I’ve installed the Release config last. If you keep your executables with the same names (i.e. no config-specific suffix), this approach will ensure that the Release executable is what you end up with. It would install the Debug executable first, then that would be overwritten by the Release executable. You don’t want that with libraries, which is why libraries should still have a config-specific suffix.

You may want to improve the above steps to only install the binaries you need for Debug, and leave out things like headers, documentation, etc. if those things are not config-specific. You could use package components for that. Or maybe it doesn’t take long to build and install them and you prefer the simpler approach of just letting them be overwritten when installing the Release config last.

1 Like

Thanks for the advices!

I was talking more from the perspective of using someone else’s packages, where I won’t have control over the cmake scripts. And I was looking for a way to install them so that my app would be able to switch between release / debug versions of the libs without me pointing directly to debug/release installation manually each time.

Example with config-specific suffixes makes total sense, but not all packages consider adding debug suffixes to the debug versions of their libraries. Though adding them not hard and maintainers might agree to add them in the next package release, but still there are lots of libraries that exist without those. So unfortunately installing two configurations to the same folder, though it seems like the best practice, couldn’t work as a general solution.

Btw, I agree that executables don’t need suffixes for this and using just Release executables should do for the most cases. There are probably cases like with Python needing python_d executable, because it implies the entire ecosystem of debug modules connected to it, but those cases are very rare. I think most of the times users don’t even need debug libs to actually debug something in those libs, they’re mostly needed to compile the main library/executable they’re working on in Debug mode.

Anyways. What vcpkg does looks really scary and fragile to me, since they’re not just moving cmake config folder - e.g. from lib/cmake/pkgshared/pkg, which already breaks some assumptions about config installation location (different number of levels, different folder names). But they’re also then patching the config files to accomodate the changes. Though I didn’t had that much experience with vcpkg to say how often it breaks, but I imagine it does happen from time to time.

So in theory the approach I’ve suggested above moves the entire installation almost as a single unit.

  • install/debug/debug/libinstall/debug/lib - most of the libraries, binaries, etc is just moved.
  • install/debug/debug/lib/cmake/pkginstall/lib/cmake/pkg - this is the only directory that’s relocated a bit (though it is the most important one). But it’s moved that way so any relative paths in moved config files are still making sense and pointing to correct debug libraries.
    E.g. Targets-release.cmake will be pointing to "${_IMPORT_PREFIX}/lib/mypkg.lib", while Targets-debug.cmake"${_IMPORT_PREFIX}/debug/bin/main.exe".
    So there’s no need to patch any files and it should just work. From the top of my head I don’t know when it would break.

Are there any alternatives really, if library didn’t provided debug suffixes?