Skip to content

<pkg>_SOURCE_DIR ignores SOURCE_SUBDIR, shadows correct cache variable #701

@vyasr

Description

@vyasr

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

cmake -S . -B build

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions