Summary
When CPMAddPackage is used with SOURCE_SUBDIR, the <pkg>_SOURCE_DIR variable propagated to the caller points to the top-level source directory, not the subdirectory where the CMake project actually lives. The project() call inside the subdirectory correctly sets <pkg>_SOURCE_DIR as a cache variable pointing to the subdirectory, but CPM's scope variable shadows the cache value, making ${<pkg>_SOURCE_DIR} unusable for locating headers or sources.
AI disclosure: I debugged this issue myself but used AI to generate the MWE below.
Expected behavior
<pkg>_SOURCE_DIR should point to the directory containing the CMakeLists.txt that was actually processed (i.e., with SOURCE_SUBDIR appended), consistent with what project() sets.
Actual behavior
The scope and cache variables disagree:
${fakepkg_SOURCE_DIR} (scope) = .../fakepkg
fakepkg_SOURCE_DIR (cache) = .../fakepkg/subdir
See the reproducer below for details on how to produce these changes.
Reproducer
File tree
cpm-source-subdir-bug/
├── CMakeLists.txt
└── fakepkg/
└── subdir/
├── CMakeLists.txt
└── include/
└── fakepkg/
└── fakepkg.h
CMakeLists.txt
cmake_minimum_required(VERSION 3.24)
project(cpm_source_subdir_mwe LANGUAGES NONE)
file(
DOWNLOAD https://github.com/cpm-cmake/CPM.cmake/releases/download/v0.42.0/CPM.cmake
${CMAKE_CURRENT_BINARY_DIR}/cmake/CPM.cmake
)
include(${CMAKE_CURRENT_BINARY_DIR}/cmake/CPM.cmake)
# fakepkg is a repo where the CMake project lives in a subdirectory ("subdir/").
# The project() call inside subdir/CMakeLists.txt is `project(fakepkg ...)`,
# which causes CMake to set `fakepkg_SOURCE_DIR` as a STATIC cache variable
# pointing to <source>/subdir.
#
# CPM propagates a scope variable fakepkg_SOURCE_DIR = <source> (the top-level
# dir, ignoring SOURCE_SUBDIR). The scope variable shadows the correct cache
# value, making ${fakepkg_SOURCE_DIR} point to the wrong directory.
CPMAddPackage(
NAME fakepkg
SOURCE_DIR ${CMAKE_CURRENT_LIST_DIR}/fakepkg
SOURCE_SUBDIR subdir
)
get_property(_cache_value CACHE fakepkg_SOURCE_DIR PROPERTY VALUE)
message(STATUS "")
message(STATUS "====== CPM + SOURCE_SUBDIR: <pkg>_SOURCE_DIR is wrong ======")
message(STATUS "")
message(STATUS " \${fakepkg_SOURCE_DIR} (scope) = ${fakepkg_SOURCE_DIR}")
message(STATUS " fakepkg_SOURCE_DIR (cache) = ${_cache_value}")
message(STATUS "")
if("${_cache_value}" STREQUAL "")
message(STATUS " NOTE: cache not set (project() in subdir may not have run)")
else()
if(NOT "${fakepkg_SOURCE_DIR}" STREQUAL "${_cache_value}")
message(STATUS " BUG: scope != cache")
message(STATUS " CPM scope var: top-level source dir (wrong)")
message(STATUS " Cache var: SOURCE_SUBDIR path (correct, set by project())")
else()
message(STATUS " OK: scope and cache agree.")
endif()
endif()
message(STATUS "")
if(EXISTS "${fakepkg_SOURCE_DIR}/include/fakepkg/fakepkg.h")
message(STATUS " \${fakepkg_SOURCE_DIR}/include/fakepkg/fakepkg.h? YES")
else()
message(STATUS " \${fakepkg_SOURCE_DIR}/include/fakepkg/fakepkg.h? NO <-- broken")
endif()
if(EXISTS "${_cache_value}/include/fakepkg/fakepkg.h")
message(STATUS " <cache>/include/fakepkg/fakepkg.h? YES <-- correct")
endif()
message(STATUS "")
message(STATUS " Root cause: CPMAddPackage propagates <pkg>_SOURCE_DIR via")
message(STATUS " PARENT_SCOPE using the top-level source dir, ignoring SOURCE_SUBDIR.")
message(STATUS " This shadows the cache variable that project() correctly sets.")
message(STATUS "=============================================================")
fakepkg/subdir/CMakeLists.txt
cmake_minimum_required(VERSION 3.14)
project(fakepkg VERSION 1.0.0 LANGUAGES NONE)
add_library(fakepkg INTERFACE)
target_include_directories(fakepkg INTERFACE
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
)
fakepkg/subdir/include/fakepkg/fakepkg.h
#pragma once
// fakepkg header
Steps to reproduce
The issue reproduces on configure, so no need to build, just
Root cause
In CPM.cmake, after FetchContent_MakeAvailable completes, CPM propagates <pkg>_SOURCE_DIR via PARENT_SCOPE using the value from FetchContent_GetProperties — which is always the top-level download/source directory, regardless of SOURCE_SUBDIR.
FetchContent_MakeAvailable internally appends SOURCE_SUBDIR to form the path passed to add_subdirectory(), but it never updates <pkg>_SOURCE_DIR to reflect this. The subdirectory's project() call does set the correct value as a cache variable, but CPM's scope variable shadows it.
Suggested fix
When SOURCE_SUBDIR is specified, CPM should append it to the <pkg>_SOURCE_DIR value before propagating via PARENT_SCOPE:
if(CPM_ARGS_SOURCE_SUBDIR)
set(${CPM_ARGS_NAME}_SOURCE_DIR "${${CPM_ARGS_NAME}_SOURCE_DIR}/${CPM_ARGS_SOURCE_SUBDIR}" PARENT_SCOPE)
endif()
Environment
- CPM.cmake v0.42.0
- CMake 4.3.1
- Ubuntu 24.04
Summary
When
CPMAddPackageis used withSOURCE_SUBDIR, the<pkg>_SOURCE_DIRvariable propagated to the caller points to the top-level source directory, not the subdirectory where the CMake project actually lives. Theproject()call inside the subdirectory correctly sets<pkg>_SOURCE_DIRas a cache variable pointing to the subdirectory, but CPM's scope variable shadows the cache value, making${<pkg>_SOURCE_DIR}unusable for locating headers or sources.AI disclosure: I debugged this issue myself but used AI to generate the MWE below.
Expected behavior
<pkg>_SOURCE_DIRshould point to the directory containing theCMakeLists.txtthat was actually processed (i.e., withSOURCE_SUBDIRappended), consistent with whatproject()sets.Actual behavior
The scope and cache variables disagree:
See the reproducer below for details on how to produce these changes.
Reproducer
File tree
CMakeLists.txtfakepkg/subdir/CMakeLists.txtfakepkg/subdir/include/fakepkg/fakepkg.hSteps to reproduce
The issue reproduces on configure, so no need to build, just
cmake -S . -B buildRoot cause
In
CPM.cmake, afterFetchContent_MakeAvailablecompletes, CPM propagates<pkg>_SOURCE_DIRviaPARENT_SCOPEusing the value fromFetchContent_GetProperties— which is always the top-level download/source directory, regardless ofSOURCE_SUBDIR.FetchContent_MakeAvailableinternally appendsSOURCE_SUBDIRto form the path passed toadd_subdirectory(), but it never updates<pkg>_SOURCE_DIRto reflect this. The subdirectory'sproject()call does set the correct value as a cache variable, but CPM's scope variable shadows it.Suggested fix
When
SOURCE_SUBDIRis specified, CPM should append it to the<pkg>_SOURCE_DIRvalue before propagating viaPARENT_SCOPE:Environment