eden/CMakeModules/CPM.cmake
crueter bd4bfc978e
WIP: [cmake] Test CPM trim
Don't merge

Signed-off-by: crueter <crueter@eden-emu.dev>
2026-06-26 01:01:36 -04:00

463 lines
15 KiB
CMake

# SPDX-FileCopyrightText: Copyright 2026 crueter
# SPDX-License-Identifier: LGPL-3.0-or-later
# CPM.cmake - CMake's missing package manager
# ===========================================
# See https://github.com/cpm-cmake/CPM.cmake for usage and update instructions.
#
# MIT License
# -----------
#[[
Copyright (c) 2019-2023 Lars Melchior and contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
]]
cmake_minimum_required(VERSION 3.31 FATAL_ERROR)
# Initialize logging prefix
if(NOT CPM_INDENT)
set(CPM_INDENT "CPM:" CACHE INTERNAL "")
endif()
get_property(CPM_INITIALIZED GLOBAL PROPERTY CPM_INITIALIZED)
if(CPM_INITIALIZED)
return()
endif()
set_property(GLOBAL PROPERTY CPM_INITIALIZED TRUE)
macro(cpm_set_policies)
# the policy allows us to change options without caching
cmake_policy(SET CMP0077 NEW)
set(CMAKE_POLICY_DEFAULT_CMP0077 NEW)
# the policy allows us to change set(CACHE) without caching
cmake_policy(SET CMP0126 NEW)
set(CMAKE_POLICY_DEFAULT_CMP0126 NEW)
# The policy uses the download time for timestamp,
# instead of the timestamp in the archive. This
# allows for proper rebuilds when a projects url changes
cmake_policy(SET CMP0135 NEW)
set(CMAKE_POLICY_DEFAULT_CMP0135 NEW)
# treat relative git repository paths as
# being relative to the parent project's remote
cmake_policy(SET CMP0150 NEW)
set(CMAKE_POLICY_DEFAULT_CMP0150 NEW)
endmacro()
cpm_set_policies()
option(CPM_DONT_UPDATE_MODULE_PATH "Don't update the module path to allow using find_package"
$ENV{CPM_DONT_UPDATE_MODULE_PATH})
set(CPM_FILE
${CMAKE_CURRENT_LIST_FILE}
CACHE INTERNAL "")
set(CPM_PACKAGES "" CACHE INTERNAL "")
if(NOT CPM_DONT_UPDATE_MODULE_PATH AND NOT DEFINED CMAKE_FIND_PACKAGE_REDIRECTS_DIR)
set(CPM_MODULE_PATH
"${CMAKE_BINARY_DIR}/CPM_modules"
CACHE INTERNAL ""
)
# remove old modules
file(REMOVE_RECURSE ${CPM_MODULE_PATH})
file(MAKE_DIRECTORY ${CPM_MODULE_PATH})
# locally added CPM modules should override global packages
set(CMAKE_MODULE_PATH "${CPM_MODULE_PATH};${CMAKE_MODULE_PATH}")
endif()
include(FetchContent)
# Create a custom FindXXX.cmake module for a CPM package This prevents `find_package(NAME)` from
# finding the system library
function(cpm_create_module_file Name)
if(NOT CPM_DONT_UPDATE_MODULE_PATH)
if(DEFINED CMAKE_FIND_PACKAGE_REDIRECTS_DIR)
# Redirect find_package calls to the CPM package. This is what FetchContent does when you set
# OVERRIDE_FIND_PACKAGE. The CMAKE_FIND_PACKAGE_REDIRECTS_DIR works for find_package in CONFIG
# mode, unlike the Find${Name}.cmake fallback. CMAKE_FIND_PACKAGE_REDIRECTS_DIR is not defined
# in script mode, or in CMake < 3.24.
# https://cmake.org/cmake/help/latest/module/FetchContent.html#fetchcontent-find-package-integration-examples
string(TOLOWER ${Name} NameLower)
file(WRITE ${CMAKE_FIND_PACKAGE_REDIRECTS_DIR}/${NameLower}-config.cmake
"include(\"\${CMAKE_CURRENT_LIST_DIR}/${NameLower}-extra.cmake\" OPTIONAL)\n"
"include(\"\${CMAKE_CURRENT_LIST_DIR}/${Name}Extra.cmake\" OPTIONAL)\n"
)
file(WRITE ${CMAKE_FIND_PACKAGE_REDIRECTS_DIR}/${NameLower}-config-version.cmake
"set(PACKAGE_VERSION_COMPATIBLE TRUE)\n" "set(PACKAGE_VERSION_EXACT TRUE)\n"
)
else()
file(WRITE ${CPM_MODULE_PATH}/Find${Name}.cmake
"include(\"${CPM_FILE}\")\n${ARGN}\nset(${Name}_FOUND TRUE)"
)
endif()
endif()
endfunction()
# checks if a package has been added before
function(cpm_check_if_package_already_added CPM_ARGS_NAME CPM_ARGS_VERSION)
if("${CPM_ARGS_NAME}" IN_LIST CPM_PACKAGES)
CPMGetPackageVersion(${CPM_ARGS_NAME} CPM_PACKAGE_VERSION)
if("${CPM_PACKAGE_VERSION}" VERSION_LESS "${CPM_ARGS_VERSION}")
message(
WARNING
"${CPM_INDENT} Requires a newer version of ${CPM_ARGS_NAME} (${CPM_ARGS_VERSION}) than currently included (${CPM_PACKAGE_VERSION})."
)
endif()
cpm_get_fetch_properties(${CPM_ARGS_NAME})
set(${CPM_ARGS_NAME}_ADDED NO)
set(CPM_PACKAGE_ALREADY_ADDED
YES
PARENT_SCOPE
)
cpm_export_variables(${CPM_ARGS_NAME})
else()
set(CPM_PACKAGE_ALREADY_ADDED
NO
PARENT_SCOPE
)
endif()
endfunction()
# Generate a PATCH_COMMAND for ExternalProject_Add() from a list of patch files. Each file is
# applied sequentially. Returns the command via a `PATCH_COMMAND` variable in the parent scope.
function(cpm_add_patches)
# Return if no patch files are supplied.
if(NOT ARGN)
return()
endif()
# Find the patch program.
find_program(PATCH_EXECUTABLE patch)
if(CMAKE_HOST_WIN32 AND NOT PATCH_EXECUTABLE)
# The Windows git executable is distributed with patch.exe. Find the path to the executable, if
# it exists, then search `../usr/bin` and `../../usr/bin` for patch.exe.
find_package(Git QUIET)
if(GIT_EXECUTABLE)
get_filename_component(extra_search_path ${GIT_EXECUTABLE} DIRECTORY)
get_filename_component(extra_search_path_1up ${extra_search_path} DIRECTORY)
get_filename_component(extra_search_path_2up ${extra_search_path_1up} DIRECTORY)
find_program(
PATCH_EXECUTABLE patch HINTS "${extra_search_path_1up}/usr/bin"
"${extra_search_path_2up}/usr/bin"
)
endif()
endif()
if(NOT PATCH_EXECUTABLE)
message(FATAL_ERROR "Couldn't find `patch` executable to use with PATCHES keyword.")
endif()
# Ensure each file exists (or error out) and add it to the list.
set(patch_args "")
set(first_item True)
foreach(PATCH_FILE ${ARGN})
# Make sure the patch file exists, if we can't find it, try again in the current directory.
if(NOT EXISTS "${PATCH_FILE}")
if(NOT EXISTS "${CMAKE_CURRENT_LIST_DIR}/${PATCH_FILE}")
message(FATAL_ERROR "Couldn't find patch file: '${PATCH_FILE}'")
endif()
set(PATCH_FILE "${CMAKE_CURRENT_LIST_DIR}/${PATCH_FILE}")
endif()
# Convert to absolute path for use with patch file command.
get_filename_component(PATCH_FILE "${PATCH_FILE}" ABSOLUTE)
# The first patch entry must be preceded by "PATCH_COMMAND" while the following items are
# preceded by "&&".
if(first_item)
set(first_item False)
list(APPEND patch_args "PATCH_COMMAND")
else()
list(APPEND patch_args "COMMAND")
endif()
# Add the patch command to the list
list(APPEND patch_args "${PATCH_EXECUTABLE}" "-p1" "-i" "${PATCH_FILE}")
endforeach()
set(PATCH_COMMAND
${patch_args}
PARENT_SCOPE
)
endfunction()
function(CPMAddPackage)
set(oneValueArgs
NAME
FORCE
VERSION
DOWNLOAD_ONLY
SOURCE_DIR
SYSTEM
EXCLUDE_FROM_ALL
SOURCE_SUBDIR
CUSTOM_CACHE_KEY
URL_HASH)
set(multiValueArgs URL OPTIONS PATCHES)
cmake_parse_arguments(CPM_ARGS "" "${oneValueArgs}" "${multiValueArgs}" "${ARGN}")
if(CPM_ARGS_DOWNLOAD_ONLY)
set(DOWNLOAD_ONLY ${CPM_ARGS_DOWNLOAD_ONLY})
else()
set(DOWNLOAD_ONLY NO)
endif()
if(NOT DEFINED CPM_ARGS_NAME)
message(FATAL_ERROR "${CPM_INDENT} NAME is required")
endif()
if(NOT DEFINED CPM_ARGS_VERSION AND NOT DEFINED CPM_ARGS_SOURCE_DIR)
message(FATAL_ERROR "${CPM_INDENT} VERSION is required")
endif()
if(NOT DEFINED CPM_ARGS_CUSTOM_CACHE_KEY AND NOT DEFINED CPM_ARGS_SOURCE_DIR)
message(FATAL_ERROR "${CPM_INDENT} CUSTOM_CACHE_KEY is required")
endif()
cpm_check_if_package_already_added(${CPM_ARGS_NAME} "${CPM_ARGS_VERSION}")
if(CPM_PACKAGE_ALREADY_ADDED)
cpm_export_variables(${CPM_ARGS_NAME})
return()
endif()
if(NOT CPM_ARGS_FORCE AND NOT "${CPM_${CPM_ARGS_NAME}_SOURCE}" STREQUAL "")
set(PACKAGE_SOURCE ${CPM_${CPM_ARGS_NAME}_SOURCE})
set(CPM_${CPM_ARGS_NAME}_SOURCE "")
CPMAddPackage(
NAME "${CPM_ARGS_NAME}"
VERSION "${CPM_ARGS_VERSION}"
SOURCE_DIR "${PACKAGE_SOURCE}"
EXCLUDE_FROM_ALL "${CPM_ARGS_EXCLUDE_FROM_ALL}"
SYSTEM "${CPM_ARGS_SYSTEM}"
PATCHES "${CPM_ARGS_PATCHES}"
OPTIONS "${CPM_ARGS_OPTIONS}"
SOURCE_SUBDIR "${CPM_ARGS_SOURCE_SUBDIR}"
DOWNLOAD_ONLY "${DOWNLOAD_ONLY}"
FORCE True
)
cpm_export_variables(${CPM_ARGS_NAME})
return()
endif()
CPMRegisterPackage("${CPM_ARGS_NAME}" "${CPM_ARGS_VERSION}")
if(DEFINED FETCHCONTENT_BASE_DIR)
set(CPM_FETCHCONTENT_BASE_DIR ${FETCHCONTENT_BASE_DIR})
else()
set(CPM_FETCHCONTENT_BASE_DIR ${CMAKE_BINARY_DIR}/_deps)
endif()
# Build patch command if any patches are specified
set(fetchContentPatchArgs "")
if(CPM_ARGS_PATCHES)
cpm_add_patches(${CPM_ARGS_PATCHES})
if(DEFINED PATCH_COMMAND)
list(APPEND fetchContentPatchArgs ${PATCH_COMMAND})
endif()
endif()
string(TOLOWER ${CPM_ARGS_NAME} lower_case_name)
if(DEFINED CPM_ARGS_SOURCE_DIR)
set(fetch_source_dir ${CPM_ARGS_SOURCE_DIR})
if(NOT IS_ABSOLUTE ${CPM_ARGS_SOURCE_DIR})
get_filename_component(
fetch_source_dir ${CPM_ARGS_SOURCE_DIR} REALPATH BASE_DIR ${CMAKE_CURRENT_BINARY_DIR}
)
endif()
if(NOT EXISTS ${fetch_source_dir})
file(REMOVE_RECURSE "${CPM_FETCHCONTENT_BASE_DIR}/${lower_case_name}-subbuild")
endif()
else()
set(download_directory
${CPM_SOURCE_CACHE}/${lower_case_name}/${CPM_ARGS_CUSTOM_CACHE_KEY})
get_filename_component(download_directory ${download_directory} ABSOLUTE)
set(fetch_source_dir ${download_directory})
endif()
set(fetchContentDeclareExtraArgs "")
if(${CPM_ARGS_EXCLUDE_FROM_ALL})
list(APPEND fetchContentDeclareExtraArgs EXCLUDE_FROM_ALL)
endif()
if(${CPM_ARGS_SYSTEM})
list(APPEND fetchContentDeclareExtraArgs SYSTEM)
endif()
if(DEFINED CPM_ARGS_SOURCE_SUBDIR)
list(APPEND fetchContentDeclareExtraArgs SOURCE_SUBDIR ${CPM_ARGS_SOURCE_SUBDIR})
endif()
if(CPM_ARGS_OPTIONS AND NOT DOWNLOAD_ONLY)
foreach(OPTION ${CPM_ARGS_OPTIONS})
cpm_parse_option("${OPTION}")
set(${OPTION_KEY} "${OPTION_VALUE}")
endforeach()
endif()
set(fetchContentURLArgs "")
if(DEFINED CPM_ARGS_URL)
list(APPEND fetchContentURLArgs URL ${CPM_ARGS_URL})
endif()
if(DEFINED CPM_ARGS_URL_HASH)
list(APPEND fetchContentURLArgs URL_HASH ${CPM_ARGS_URL_HASH})
endif()
if(NOT DEFINED CPM_ARGS_SOURCE_DIR)
if(EXISTS ${download_directory})
set(PACKAGE_INFO "${PACKAGE_INFO} at ${download_directory}")
else()
file(REMOVE_RECURSE ${CPM_FETCHCONTENT_BASE_DIR}/${lower_case_name}-subbuild)
set(PACKAGE_INFO "${PACKAGE_INFO} to ${download_directory}")
endif()
file(LOCK ${download_directory}/../cmake.lock)
endif()
if(NOT "${DOWNLOAD_ONLY}")
cpm_create_module_file(${CPM_ARGS_NAME} "CPMAddPackage(\"${ARGN}\")")
endif()
FetchContent_Declare(${CPM_ARGS_NAME}
${fetchContentDeclareExtraArgs}
${fetchContentURLArgs}
${fetchContentPatchArgs}
SOURCE_DIR ${fetch_source_dir}
DOWNLOAD_NO_PROGRESS TRUE
)
if(DOWNLOAD_ONLY)
FetchContent_Populate(${CPM_ARGS_NAME}
${fetchContentURLArgs}
SOURCE_DIR "${fetch_source_dir}"
BINARY_DIR "${CPM_FETCHCONTENT_BASE_DIR}/${lower_case_name}-build"
SUBBUILD_DIR "${CPM_FETCHCONTENT_BASE_DIR}/${lower_case_name}-subbuild"
DOWNLOAD_NO_PROGRESS TRUE
)
else()
FetchContent_MakeAvailable(${CPM_ARGS_NAME})
endif()
if(NOT DEFINED CPM_ARGS_SOURCE_DIR)
file(LOCK ${download_directory}/../cmake.lock RELEASE)
endif()
FetchContent_GetProperties(${CPM_ARGS_NAME})
cpm_store_fetch_properties(
${CPM_ARGS_NAME} ${${lower_case_name}_SOURCE_DIR} ${${lower_case_name}_BINARY_DIR}
)
cpm_get_fetch_properties("${CPM_ARGS_NAME}")
set(${CPM_ARGS_NAME}_ADDED YES)
cpm_export_variables("${CPM_ARGS_NAME}")
endfunction()
# export variables available to the caller to the
# parent scope expects ${CPM_ARGS_NAME} to be set
macro(cpm_export_variables name)
set(${name}_SOURCE_DIR
"${${name}_SOURCE_DIR}"
PARENT_SCOPE
)
set(${name}_BINARY_DIR
"${${name}_BINARY_DIR}"
PARENT_SCOPE
)
set(${name}_ADDED
"${${name}_ADDED}"
PARENT_SCOPE
)
set(CPM_LAST_PACKAGE_NAME
"${name}"
PARENT_SCOPE
)
endmacro()
# registers a package that has been added to CPM
function(CPMRegisterPackage PACKAGE VERSION)
list(APPEND CPM_PACKAGES ${PACKAGE})
set(CPM_PACKAGES
${CPM_PACKAGES}
CACHE INTERNAL ""
)
set("CPM_PACKAGE_${PACKAGE}_VERSION"
${VERSION}
CACHE INTERNAL ""
)
endfunction()
# retrieve the current version of the package to ${OUTPUT}
function(CPMGetPackageVersion PACKAGE OUTPUT)
set(${OUTPUT}
"${CPM_PACKAGE_${PACKAGE}_VERSION}"
PARENT_SCOPE
)
endfunction()
function(cpm_get_fetch_properties PACKAGE)
set(${PACKAGE}_SOURCE_DIR
"${CPM_PACKAGE_${PACKAGE}_SOURCE_DIR}"
PARENT_SCOPE
)
set(${PACKAGE}_BINARY_DIR
"${CPM_PACKAGE_${PACKAGE}_BINARY_DIR}"
PARENT_SCOPE
)
endfunction()
function(cpm_store_fetch_properties PACKAGE source_dir binary_dir)
set(CPM_PACKAGE_${PACKAGE}_SOURCE_DIR
"${source_dir}"
CACHE INTERNAL ""
)
set(CPM_PACKAGE_${PACKAGE}_BINARY_DIR
"${binary_dir}"
CACHE INTERNAL ""
)
endfunction()
# splits a package option
function(cpm_parse_option OPTION)
string(REGEX MATCH "^[^ ]+" OPTION_KEY "${OPTION}")
string(LENGTH "${OPTION}" OPTION_LENGTH)
string(LENGTH "${OPTION_KEY}" OPTION_KEY_LENGTH)
if(OPTION_KEY_LENGTH STREQUAL OPTION_LENGTH)
# no value for key provided, assume user wants to set option to "ON"
set(OPTION_VALUE "ON")
else()
math(EXPR OPTION_KEY_LENGTH "${OPTION_KEY_LENGTH}+1")
string(SUBSTRING "${OPTION}" "${OPTION_KEY_LENGTH}" "-1" OPTION_VALUE)
endif()
set(OPTION_KEY
"${OPTION_KEY}"
PARENT_SCOPE
)
set(OPTION_VALUE
"${OPTION_VALUE}"
PARENT_SCOPE
)
endfunction()