GNUInstallDirs: Special CMAKE_INSTALL_FULL_<dir> contain double slash for CMAKE_INSTALL_PREFIX starting with /opt when CMP0192 is OLD

Hi all,

I believe I’ve run into a small bug in GNUInstallDirs and wanted to check here before opening an issue.

When CMAKE_INSTALL_PREFIX is under /opt/ and CMP0192 is OLD or unset, the FHS special case produces a doubled separator. CMAKE_INSTALL_FULL_SYSCONFDIR comes out as /etc//opt/foo instead of /etc/opt/foo, and CMAKE_INSTALL_FULL_LOCALSTATEDIR and CMAKE_INSTALL_FULL_RUNSTATEDIR have the same problem. The special cases documentation says “the SYSCONFDIR value etc becomes /etc/opt/...”, so I don’t think the // is intended. I noticed it because installing to CMAKE_INSTALL_FULL_SYSCONFDIR triggers the CMP0177 warning, since normalizing would change the path.

cmake_minimum_required(VERSION 3.25)
project(Foo C)
set(CMAKE_INSTALL_PREFIX /opt/foo)
include(GNUInstallDirs)
message("SYSCONFDIR:    ${CMAKE_INSTALL_FULL_SYSCONFDIR}")
message("LOCALSTATEDIR: ${CMAKE_INSTALL_FULL_LOCALSTATEDIR}")
message("RUNSTATEDIR:   ${CMAKE_INSTALL_FULL_RUNSTATEDIR}")
install(FILES CMakeLists.txt DESTINATION ${CMAKE_INSTALL_FULL_SYSCONFDIR})

Output:

SYSCONFDIR:    /etc//opt/foo
LOCALSTATEDIR: /var//opt/foo
RUNSTATEDIR:   /var/run//opt/foo
CMake Warning (policy) at CMakeLists.txt:8 (install):
  Policy CMP0177 is not set: install() DESTINATION paths are normalized.  Run
  "cmake --help-policy CMP0177" for policy details.  Use the cmake_policy
  command to set the policy and suppress this warning.
This warning is for project developers.  Use -Wno-author or -Wno-policy to
suppress it.

Setting CMP0177 silences the warning, but the // is still there. Only CMP0192 NEW gives clean values. Results with a fresh build directory each time:

CMP0177 CMP0192 CMP0177 warning FULL_<dir> values
unset unset yes contain //
unset OLD yes contain //
unset NEW no normalized
OLD or NEW unset or OLD no contain //
OLD or NEW NEW no normalized

From reading Modules/GNUInstallDirs.cmake, I think the cause is _GNUInstallDirs_special_absolute, which builds the path as "/${original_path}/${install_prefix}". The prefix already starts with /, so the result is /etc//opt/foo. Both policy settings go through this helper. The difference is whether the result gets normalized afterwards:

  • CMP0192 NEW: __GNUInstallDirs_default_in_root applies the special case when computing the default CMAKE_INSTALL_SYSCONFDIR, then runs cmake_path(NORMAL_PATH), and /etc/opt/foo is cached. That value is already absolute, so GNUInstallDirs_get_absolute_install_dir copies it into CMAKE_INSTALL_FULL_SYSCONFDIR unchanged.
  • CMP0192 OLD: the cached default stays relative (etc), and GNUInstallDirs_get_absolute_install_dir calls _GNUInstallDirs_special_absolute itself without normalizing, so the // ends up in CMAKE_INSTALL_FULL_SYSCONFDIR.

So as far as I can tell, CMP0192 behaves as documented. Its NEW behavior just happens to hide the doubled separator. The RunCMake GNUInstallDirs tests set CMP0192 NEW for every case, which may be why the OLD path wasn’t caught.

I’m using CMake 4.4.4 on Ubuntu 26.04.1 LTS, and the same code looks unchanged on current master commit (5856adc). I think the cleanest fix is to normalize the result in GNUInstallDirs_get_absolute_install_dir, either just for the special-case directories or for every FULL_<dir>, depending on how much of a behavior change is acceptable. Normalizing inside _GNUInstallDirs_special_absolute would also work.

Does this look like a bug worth an issue, or am I missing something? I’m happy to open an issue or put together a merge request, including a test for the CMP0192 OLD case.

Thanks for the explanation. Yes, I think opening an issue for this seems appropriate. If you’re happy to make a merge request to fix it too, that would also be welcome.

Thanks! I’ve opened #28186 and submitted a merge request with a fix and additional tests: !12673