Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ jobs:
- name: Run provider merging tests
run: cmake -P tests/install/test_provider_merge.cmake

- name: Run setup.cmake version resolution test
run: cmake -P tests/setup/test_setup_version_resolution.cmake

integration-tests:
name: Integration Tests
runs-on: ubuntu-latest
Expand Down Expand Up @@ -152,4 +155,3 @@ jobs:
test -f templates/Doxyfile.in
test -f templates/custom.css
echo "✓ All template files present"

47 changes: 40 additions & 7 deletions setup.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -9,22 +9,55 @@

cmake_minimum_required(VERSION 3.20)

# Detect cpp-library version from git tags
# Extract latest semantic version from a list of refs/tags/vX.Y.Z entries
function(extract_latest_cpp_library_version_from_tags TAG_REFS OUTPUT_VAR)
string(REGEX MATCHALL "refs/tags/v[0-9]+\\.[0-9]+\\.[0-9]+" TAG_REFS_MATCHES "${TAG_REFS}")
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unanchored regex matches pre-release tags as releases

Low Severity

The MATCHALL regex refs/tags/v[0-9]+\\.[0-9]+\\.[0-9]+ has no end-of-token anchor, so it performs substring matching. A pre-release tag like refs/tags/v2.0.0-rc1 would partially match as refs/tags/v2.0.0, causing the function to return version 2.0.0 even if only the -rc1 tag exists. CPM would then attempt to check out the nonexistent v2.0.0 tag and fail — the same class of problem this PR aims to fix.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit dcab025. Configure here.


set(SEMVER_TAGS "")
foreach(tag_ref IN LISTS TAG_REFS_MATCHES)
string(REGEX REPLACE "^refs/tags/v" "" semver "${tag_ref}")
list(APPEND SEMVER_TAGS "${semver}")
endforeach()

if(SEMVER_TAGS)
list(REMOVE_DUPLICATES SEMVER_TAGS)
list(SORT SEMVER_TAGS COMPARE NATURAL ORDER DESCENDING)
list(GET SEMVER_TAGS 0 latest_version)
set(${OUTPUT_VAR} "${latest_version}" PARENT_SCOPE)
else()
set(${OUTPUT_VAR} "" PARENT_SCOPE)
endif()
endfunction()

# Detect cpp-library version from local git tags first
set(CPP_LIBRARY_VERSION "")
execute_process(
COMMAND git describe --tags --abbrev=0
WORKING_DIRECTORY ${CMAKE_CURRENT_LIST_DIR}
OUTPUT_VARIABLE CPP_LIBRARY_GIT_VERSION
OUTPUT_STRIP_TRAILING_WHITESPACE
ERROR_QUIET
)

# Clean version (remove 'v' prefix if present)
if(CPP_LIBRARY_GIT_VERSION)
string(REGEX REPLACE "^v" "" CPP_LIBRARY_VERSION "${CPP_LIBRARY_GIT_VERSION}")
else()
# Fallback to X.Y.Z placeholder if no git tag found
set(CPP_LIBRARY_VERSION "X.Y.Z")
message(WARNING "No git tag found for cpp-library version. Using placeholder 'X.Y.Z'. Check https://github.com/stlab/cpp-library/releases for the latest version.")
endif()

# If setup.cmake is downloaded standalone (no local tags), detect from remote tags
if(NOT CPP_LIBRARY_VERSION)
execute_process(
COMMAND git ls-remote --tags --refs https://github.com/stlab/cpp-library.git v[0-9]*
OUTPUT_VARIABLE CPP_LIBRARY_REMOTE_TAGS
OUTPUT_STRIP_TRAILING_WHITESPACE
ERROR_QUIET
TIMEOUT 10
)
extract_latest_cpp_library_version_from_tags("${CPP_LIBRARY_REMOTE_TAGS}" CPP_LIBRARY_VERSION)
endif()

# Last-resort fallback to main branch so initialization still works
if(NOT CPP_LIBRARY_VERSION)
set(CPP_LIBRARY_VERSION "main")
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fallback @main produces invalid CPM checkout tag

High Severity

The last-resort fallback sets CPP_LIBRARY_VERSION to "main", which gets interpolated into the template as CPMAddPackage("gh:stlab/cpp-library@main"). In CPM.cmake's shorthand syntax, the @ operator sets the VERSION parameter, and CPM derives GIT_TAG by prepending v — so @main resolves to GIT_TAG=vmain, a tag that doesn't exist. The correct syntax for a branch reference is #main, not @main. This means the fallback path still produces an unusable checkout, which is exactly the bug this PR aims to fix.

Additional Locations (2)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit dcab025. Configure here.

message(WARNING "No git tag found for cpp-library version. Falling back to branch 'main'.")
endif()

message(STATUS "cpp-library version: ${CPP_LIBRARY_VERSION}")
Expand Down
65 changes: 65 additions & 0 deletions tests/setup/test_setup_version_resolution.cmake
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# SPDX-License-Identifier: BSL-1.0
#
# Integration test for setup.cmake version detection when run standalone
#
# Run as: cmake -P tests/setup/test_setup_version_resolution.cmake

cmake_minimum_required(VERSION 3.20)

if(DEFINED ENV{TMPDIR} AND NOT "$ENV{TMPDIR}" STREQUAL "")
set(TEST_BASE_DIR "$ENV{TMPDIR}")
elseif(DEFINED ENV{TEMP} AND NOT "$ENV{TEMP}" STREQUAL "")
set(TEST_BASE_DIR "$ENV{TEMP}")
elseif(DEFINED ENV{TMP} AND NOT "$ENV{TMP}" STREQUAL "")
set(TEST_BASE_DIR "$ENV{TMP}")
else()
set(TEST_BASE_DIR "${CMAKE_CURRENT_BINARY_DIR}")
endif()

set(TEST_ROOT_DIR "${TEST_BASE_DIR}/cpp-library-setup-version-test")
set(TEST_PROJECT_NAME "setup-version-test-lib")

file(REMOVE_RECURSE "${TEST_ROOT_DIR}")
file(MAKE_DIRECTORY "${TEST_ROOT_DIR}")
file(COPY "${CMAKE_CURRENT_LIST_DIR}/../../setup.cmake" DESTINATION "${TEST_ROOT_DIR}")

execute_process(
COMMAND
${CMAKE_COMMAND}
-P
setup.cmake
--
--name=${TEST_PROJECT_NAME}
--namespace=testns
--description=test
--header-only=yes
--examples=no
--tests=no
WORKING_DIRECTORY "${TEST_ROOT_DIR}"
RESULT_VARIABLE SETUP_RESULT
OUTPUT_VARIABLE SETUP_OUTPUT
ERROR_VARIABLE SETUP_ERROR
)

if(NOT SETUP_RESULT EQUAL 0)
message(FATAL_ERROR "setup.cmake failed with exit code ${SETUP_RESULT}\nstdout:\n${SETUP_OUTPUT}\nstderr:\n${SETUP_ERROR}")
endif()

set(GENERATED_CMAKE_LISTS "${TEST_ROOT_DIR}/${TEST_PROJECT_NAME}/CMakeLists.txt")
if(NOT EXISTS "${GENERATED_CMAKE_LISTS}")
message(FATAL_ERROR "Generated CMakeLists.txt not found: ${GENERATED_CMAKE_LISTS}")
endif()

file(READ "${GENERATED_CMAKE_LISTS}" GENERATED_CONTENT)

if(GENERATED_CONTENT MATCHES "CPMAddPackage\\(\"gh:stlab/cpp-library@X\\.Y\\.Z\"\\)")
message(FATAL_ERROR "setup.cmake generated placeholder version X.Y.Z, which should never be emitted.")
endif()

if(NOT GENERATED_CONTENT MATCHES "CPMAddPackage\\(\"gh:stlab/cpp-library@([0-9]+\\.[0-9]+\\.[0-9]+|main)\"\\)")
message(FATAL_ERROR "setup.cmake generated invalid cpp-library version reference in CPMAddPackage.")
endif()

message(STATUS "✓ setup.cmake generated a valid cpp-library version reference")

file(REMOVE_RECURSE "${TEST_ROOT_DIR}")