You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

1163 lines
32 KiB

# SPDX-FileCopyrightText: Copyright 2026 crueter
# SPDX-License-Identifier: LGPL-3.0-or-later
cmake_minimum_required(VERSION 3.31)
if(MSVC OR ANDROID OR IOS)
set(BUNDLED_DEFAULT ON)
else()
set(BUNDLED_DEFAULT OFF)
endif()
# TODO: Handle nested cpmutil includes
# Root directory detection
if (CMAKE_SCRIPT_MODE_FILE)
# In script mode, default to PWD
# Or you can specify one yourself
if (NOT DEFINED CPMUTIL_ROOT)
set(CPMUTIL_ROOT ${CMAKE_SOURCE_DIR})
endif()
else()
# A project exists, we can use its directory
set(CPMUTIL_ROOT ${PROJECT_SOURCE_DIR})
endif()
set(CPM_SOURCE_CACHE "${CPMUTIL_ROOT}/.cache/cpm"
CACHE PATH "Directory to download CPM dependencies")
option(CPMUTIL_FORCE_BUNDLED
"Force bundled packages for all CPM depdendencies" ${BUNDLED_DEFAULT})
option(CPMUTIL_FORCE_SYSTEM
"Force system packages for all CPM dependencies" OFF)
set(CPMUTIL_PATCH_DIR "${CPMUTIL_ROOT}/.patch" CACHE STRING
"Directory containing patches for packages")
include(CPM)
# Rudimentary target architecture detection
# This is in script mode, CI packages will be nonfunctional. TODO
if (NOT DEFINED CMAKE_SYSTEM_PROCESSOR)
message(DEBUG "[CPMUtil] No system architecture found, "
"CI packages are nonfunctional")
elseif (NOT DEFINED ARCHITECTURE)
string(TOLOWER ${CMAKE_SYSTEM_PROCESSOR} processor)
if (processor MATCHES "x86|amd64")
set(CPMUTIL_AMD64 ON)
elseif(processor MATCHES "^aarch64|^arm64|^armv8\.*")
set(CPMUTIL_ARM64 ON)
elseif(processor MATCHES "riscv")
set(CPMUTIL_RISCV64 ON)
endif()
else()
# This block exists for compatibility with my own DetectArchitecture.cmake.
if (ARCHITECTURE_x86_64)
set(CPMUTIL_AMD64 ON)
elseif(ARCHITECTURE_arm64)
set(CPMUTIL_ARM64 ON)
elseif(ARCHITECTURE_riscv64)
set(CPMUTIL_RISCV64 ON)
endif()
endif()
# cpmfile parsing
set(CPMUTIL_JSON_FILE "${CPMUTIL_ROOT}/cpmfile.json")
if(EXISTS ${CPMUTIL_JSON_FILE})
file(READ ${CPMUTIL_JSON_FILE} CPMFILE_CONTENT)
# Script mode doesn't support add_custom_target etc.
if (NOT CMAKE_SCRIPT_MODE_FILE)
if (NOT TARGET cpmfiles)
add_custom_target(cpmfiles)
endif()
target_sources(cpmfiles PRIVATE ${CPMUTIL_JSON_FILE})
set_property(DIRECTORY APPEND PROPERTY
CMAKE_CONFIGURE_DEPENDS
"${CPMUTIL_JSON_FILE}")
endif()
else()
message(DEBUG "[CPMUtil] cpmfile ${CPMUTIL_JSON_FILE} "
"does not exist, AddJsonPackage will be a no-op")
endif()
#############
# UTILITIES #
#############
# Analogous to POSIX echo
macro(echo)
string(REPLACE ";" " " message "${ARGN}")
if (CMAKE_SCRIPT_MODE_FILE)
execute_process(COMMAND ${CMAKE_COMMAND} -E echo
"${message}")
else()
message(STATUS "${message}")
endif()
endmacro()
# Analogous to POSIX echo, but to stderr
macro(echo_error)
string(REPLACE ";" " " message "${ARGN}")
message(NOTICE "${message}")
endmacro()
# Fatal error, accounting for script mode
macro(fatal)
if (CMAKE_SCRIPT_MODE_FILE)
echo_error(${ARGN})
cmake_language(EXIT 1)
else()
message(FATAL_ERROR ${ARGN})
endif()
endmacro()
# Analogous to POSIX sleep
macro(sleep time)
execute_process(COMMAND ${CMAKE_COMMAND} -E sleep ${time})
endmacro()
# Analogous to GNU mktemp, with fallbacks
function(mktempdir out)
# shell out to system mktemp if available
find_program(MKTEMP_EXECUTABLE mktemp)
if (MKTEMP_EXECUTABLE)
execute_process(COMMAND mktemp -d
OUTPUT_VARIABLE dir
OUTPUT_STRIP_TRAILING_WHITESPACE
RESULT_VARIABLE ret)
if (ret EQUAL 0)
set(${out} "${dir}" PARENT_SCOPE)
return()
endif()
endif()
string(RANDOM LENGTH 10 rand_str)
set(tmp_str "tmp.${rand_str}")
# create something in /tmp if it exists
if(EXISTS "/tmp" AND IS_DIRECTORY "/tmp")
set(dir "/tmp/${tmp_str}")
file(MAKE_DIRECTORY "${dir}" RESULT res)
if (res EQUAL 0)
set(${out} "${dir}" PARENT_SCOPE)
return()
endif()
endif()
# tmpdir does not exist, extremely legacy mode
set(dir "${CMAKE_CURRENT_LIST_DIR}/.tmp/${tmp_str}")
file(MAKE_DIRECTORY "${dir}" RESULT res)
if (res EQUAL 0)
set(${out} "${dir}" PARENT_SCOPE)
return()
endif()
fatal("Fatal: Could not create temporary directory. "
"Check write permissions to the current directory")
endfunction()
# Get a package's effective URL.
function(get_package_url)
set(oneValueArgs
GIT_HOST
REPO
VERSION
ARTIFACT
PACKAGE
URL_OUT
GIT_URL_OUT)
cmake_parse_arguments(ARG "" "${oneValueArgs}" "" ${ARGN})
if (NOT DEFINED ARG_URL_OUT)
fatal("get_package_url: URL_OUT is required")
endif()
if(NOT DEFINED ARG_GIT_HOST)
set(ARG_GIT_HOST github.com)
endif()
if(DEFINED ARG_REPO)
set(url https://${ARG_GIT_HOST}/${ARG_REPO})
if (DEFINED ARG_GIT_URL_OUT)
set(${ARG_GIT_URL_OUT} "${url}")
endif()
if(DEFINED ARG_ARTIFACT)
set(url
"${url}/releases/download/${ARG_VERSION}/${ARG_ARTIFACT}")
else()
set(url "${url}/archive/${ARG_VERSION}.tar.gz")
endif()
else()
fatal("${package}: No URL or repository defined")
endif()
set(${ARG_URL_OUT} "${url}")
return(PROPAGATE ${ARG_URL_OUT} ${ARG_GIT_URL_OUT})
endfunction()
# Get a package's cache path.
function(get_cache_path package key out)
string(TOLOWER ${package} lower_name)
# TODO: Figure out a sln to CPM_SOURCE_CACHE; CPMConfig.cmake?
set(${out} ${CPM_SOURCE_CACHE}/${lower_name}/${key})
return(PROPAGATE ${out})
endfunction()
# Download a URL to file, with a sha512 hash
# And retry 5 times
function(cpm_download url file)
list(LENGTH ARGN argn_len)
if(argn_len GREATER 0)
list(GET ARGN 0 hash)
set(args EXPECTED_HASH SHA512=${hash})
else()
unset(args)
endif()
foreach(i RANGE 5)
file(DOWNLOAD ${url} ${file}
${args}
STATUS ret
LOG log)
list(GET ret 0 code)
if (code EQUAL 0)
break()
endif()
echo_error("Download failed. Trying again in 5 seconds")
sleep(5)
endforeach()
# TODO: use return code or something
if (NOT code EQUAL 0)
fatal("Download for ${pkg_url} failed after 5 tries")
endif()
endfunction()
# Wrapper around find_program that works with Git for Windows
macro(cpm_find_program)
# Windows needs additional paths for some utilities.
if (CMAKE_HOST_WIN32)
find_package(Git QUIET)
if(Git_FOUND)
# Search within the Git for Windows paths.
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)
set(base_hints "${extra_search_path_1up}/usr/bin"
"${extra_search_path_2up}/usr/bin")
# Also add core_perl to the paths, for perl commands like json_pp
set(hints "")
foreach(hint ${base_hints})
list(APPEND hints
"${hint}"
"${hint}/core_perl")
endforeach()
find_program(${ARGN} HINTS ${hints})
endif()
# If no Git is found, continue as normal
endif()
find_program(${ARGN})
endmacro()
# Apply patches to a directory.
function(apply_patches patches dir)
cpm_find_program(PATCH_EXE patch)
if (NOT PATCH_EXE)
fatal("Could not find patch executable")
endif()
foreach(patch ${patches})
get_filename_component(patch_name ${patch} NAME)
echo("-- Applying patch ${patch_name}")
execute_process(
COMMAND ${PATCH_EXE} -p1
INPUT_FILE ${patch}
WORKING_DIRECTORY ${dir})
endforeach()
endfunction()
# Fetches a file to the CPM source cache.
function(fetch_package)
set(oneValueArgs
URL
HASH
PATH
PATCH_KEY)
set(multiValueArgs PATCHES)
set(optionArgs FORCE)
cmake_parse_arguments(ARG
"${optionArgs}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN})
if (NOT DEFINED ARG_URL)
fatal("fetch_package: URL is required")
endif()
if (NOT DEFINED ARG_HASH)
fatal("fetch_package: HASH is required")
endif()
if (NOT DEFINED ARG_PATH)
fatal("fetch_package: PATH is required")
endif()
if (NOT DEFINED ARG_PATCH_KEY)
compute_patch_key("${ARG_PATCHES}" ARG_PATCH_KEY)
endif()
# refetch if cache is invalid or forced
needs_refetch(${ARG_PATH} "${ARG_PATCH_KEY}" CACHE_INVALID)
if (ARG_FORCE OR CACHE_INVALID)
file(REMOVE_RECURSE ${ARG_PATH})
else()
return()
endif()
# Temporary directory.
mktempdir(TMP)
# Get filename from URL
get_filename_component(base_filename ${ARG_URL} NAME)
# Download
set(file ${TMP}/${base_filename})
cpm_download(${ARG_URL} ${file} ${ARG_HASH})
message(DEBUG "Downloaded ${base_filename}")
# Extract the downloaded archive
# TODO: Moar error handling
set(dir ${TMP}/${base_filename}-extracted)
file(MAKE_DIRECTORY ${dir})
file(ARCHIVE_EXTRACT
INPUT ${file}
DESTINATION ${dir})
# This is copied near-verbatim from ExternalProject/extractfile.cmake.in
# If there's just one subdirectory and nothing else, move it
file(GLOB contents "${dir}/*")
list(REMOVE_ITEM contents "${dir}/.DS_Store")
list(LENGTH contents n)
# If n == 1 and contents points to a directory, this is a GitHub-style pack
# In this case contents points to the subdir which will get renamed
# If not, contents will point to the parent dir which will get renamed
if (NOT n EQUAL 1 OR NOT IS_DIRECTORY "${contents}")
set(contents "${dir}")
endif()
file(REAL_PATH "${contents}" contents_abs)
# paths
cmake_path(ABSOLUTE_PATH ARG_PATH
NORMALIZE
OUTPUT_VARIABLE abs_path)
cmake_path(GET abs_path PARENT_PATH path_parent)
cmake_path(GET abs_path FILENAME path_name)
# rename tmp dir
set(tmp_renamed "${TMP}/${path_name}")
file(RENAME "${contents_abs}" "${tmp_renamed}")
# now copy
# TODO: Error handling beyond what cmake does????
file(COPY ${tmp_renamed} DESTINATION ${path_parent})
# TODO: only echo this in script mode
message(DEBUG "Extracted to ${abs_path}")
# Apply patches
apply_patches("${ARG_PATCHES}" "${abs_path}")
# Write patch key
file(WRITE "${abs_path}/.cpm_patch_key" ${ARG_PATCH_KEY})
# done! :)
file(REMOVE_RECURSE ${TMP})
endfunction()
# compute a hash of all patch file contents
# if there are no patches, returns none
function(compute_patch_key patches out)
if(NOT patches)
set("${out}" "none" PARENT_SCOPE)
return()
endif()
set(combined "")
foreach(PATCH ${patches})
file(READ "${PATCH}" contents)
string(APPEND combined "${contents}")
endforeach()
string(SHA512 key "${combined}")
set("${out}" "${key}" PARENT_SCOPE)
endfunction()
# Check if a package needs to be redownloaded. This can occur if:
# - Patch key is missing or mismatched
# - Package fetch dir is missing
# path is the package cache path
function(needs_refetch path patch_key out)
set(patch_key_file "${path}/.cpm_patch_key")
# download directory is empty or patch key file is missing
if (NOT EXISTS ${patch_key_file})
set(${out} TRUE PARENT_SCOPE)
return()
endif()
# compare patch key
file(READ "${patch_key_file}" current_patch_key)
if (NOT current_patch_key STREQUAL patch_key)
set(${out} TRUE PARENT_SCOPE)
else()
set(${out} FALSE PARENT_SCOPE)
endif()
endfunction()
# Message
function(cpm_utils_message level)
string(REPLACE ";" " " message "${ARGN}")
message(${level} "[CPMUtil] ${message}")
endfunction()
# propagate a variable to parent scope
macro(Propagate var)
set(${var} ${${var}} PARENT_SCOPE)
endmacro()
# Get cpmfile content
function(get_cpmfile_content out)
set(${out} "${CPMFILE_CONTENT}" PARENT_SCOPE)
endfunction()
# Get absolute path to cpmfile
function(get_cpmfile_path out)
file(REAL_PATH ${CPMUTIL_JSON_FILE} ${out})
return(PROPAGATE ${out})
endfunction()
# idk
function(array_to_list array length out)
math(EXPR range "${length} - 1")
foreach(IDX RANGE ${range})
string(JSON _element GET "${array}" "${IDX}")
list(APPEND NEW_LIST ${_element})
endforeach()
set("${out}" "${NEW_LIST}" PARENT_SCOPE)
endfunction()
# json
function(get_json_element object out member)
list(LENGTH ARGN argn_len)
if(argn_len GREATER 0)
list(GET ARGN 0 default)
endif()
string(JSON out_type ERROR_VARIABLE err TYPE "${object}" ${member})
if(err)
if (DEFINED default)
set("${out}" "${default}" PARENT_SCOPE)
else()
unset("${out}" PARENT_SCOPE)
endif()
return()
endif()
string(JSON outvar GET "${object}" ${member})
if(out_type STREQUAL "ARRAY")
string(JSON _len LENGTH "${object}" ${member})
set("${out}_LENGTH" "${_len}" PARENT_SCOPE)
endif()
set("${out}" "${outvar}" PARENT_SCOPE)
endfunction()
# register a package to CPMUtil's global registry
function(cpmutil_register_package name url version)
set_property(GLOBAL APPEND PROPERTY CPM_PACKAGE_NAMES ${name})
set_property(GLOBAL APPEND PROPERTY CPM_PACKAGE_URLS ${url})
set_property(GLOBAL APPEND PROPERTY CPM_PACKAGE_SHAS "${version}")
endfunction()
# Determine whether or not a package has a viable system candidate.
function(SystemPackageViable JSON_NAME)
string(JSON object GET "${CPMFILE_CONTENT}" "${JSON_NAME}")
parse_object(${object})
string(REPLACE " " ";" find_args "${find_args}")
if(${package}_FORCE_BUNDLED OR
(CPMUTIL_FORCE_BUNDLED
AND NOT ${package}_FORCE_SYSTEM
AND NOT CPMUTIL_FORCE_SYSTEM))
set(${package}_FOUND OFF)
else()
find_package(${package} ${min_version} ${find_args} QUIET NO_POLICY_SCOPE)
endif()
set(${JSON_NAME}_VIABLE ${${package}_FOUND} PARENT_SCOPE)
set(${JSON_NAME}_PACKAGE ${package} PARENT_SCOPE)
endfunction()
# Add several packages such that if one is bundled,
# all the rest must also be bundled.
function(AddDependentPackages)
set(_some_system OFF)
set(_some_bundled OFF)
foreach(pkg ${ARGN})
SystemPackageViable(${pkg})
if (${pkg}_VIABLE)
set(_some_system ON)
list(APPEND _system_pkgs ${${pkg}_PACKAGE})
else()
set(_some_bundled ON)
list(APPEND _bundled_pkgs ${${pkg}_PACKAGE})
endif()
endforeach()
if (_some_system AND _some_bundled)
foreach(pkg ${ARGN})
list(APPEND package_names ${${pkg}_PACKAGE})
endforeach()
string(REPLACE ";" ", " package_names "${package_names}")
string(REPLACE ";" ", " bundled_names "${_bundled_pkgs}")
foreach(sys ${_system_pkgs})
list(APPEND system_names ${sys}_FORCE_BUNDLED)
endforeach()
string(REPLACE ";" ", " system_names "${system_names}")
message(FATAL_ERROR "Partial dependency installation detected "
"for the following packages:\n${package_names}\n"
"You can solve this in one of two ways:\n"
"1. Install or upgrade the following packages "
"to your system if available:"
"\n\t${bundled_names}\n"
"2. Set the following variables to ON:"
"\n\t${system_names}\n"
"This may also be caused by a version mismatch, "
"such as one package being newer than the other.")
endif()
foreach(pkg ${ARGN})
AddJsonPackage(${pkg})
endforeach()
endfunction()
# process version/artifact replacements
macro(process_version_replacements)
# numeric version text replacement
if (numeric_version)
string(REPLACE "%NUMERIC_VERSION%" "${numeric_version}"
version ${version})
string(REPLACE "%NUMERIC_VERSION%" "${numeric_version}"
artifact ${artifact})
endif()
# version text replacement
if (artifact)
string(REPLACE "%VERSION%" "${version}"
artifact ${artifact})
endif()
endmacro()
# json util
macro(parse_object object)
get_json_element("${object}" package package ${JSON_NAME})
get_json_element("${object}" repo repo)
get_json_element("${object}" ci ci OFF)
get_json_element("${object}" version version)
get_json_element("${object}" min_version min_version)
get_json_element("${object}" git_host git_host "github.com")
if (NOT version)
cpm_utils_message(FATAL_ERROR "${JSON_NAME}: version is required")
endif()
if(ci)
get_json_element("${object}" name name "${JSON_NAME}")
get_json_element("${object}" extension extension "tar.zst")
get_json_element("${object}" raw_disabled disabled_platforms)
if(raw_disabled)
array_to_list("${raw_disabled}"
${raw_disabled_LENGTH} disabled_platforms)
else()
set(disabled_platforms "")
endif()
else()
# TODO: correct hash if missing
get_json_element("${object}" hash hash)
get_json_element("${object}" url url)
get_json_element("${object}" artifact artifact)
get_json_element("${object}" source_subdir source_subdir)
get_json_element("${object}" bundled bundled "unset")
get_json_element("${object}" numeric_version numeric_version)
get_json_element("${object}" find_args find_args)
get_json_element("${object}" skip_updates skip_updates OFF)
get_json_element("${object}" raw_patches patches)
process_version_replacements()
# format patchdir
unset(patches)
if(raw_patches)
math(EXPR range "${raw_patches_LENGTH} - 1")
foreach(IDX RANGE ${range})
string(JSON _patch GET "${raw_patches}" "${IDX}")
set(full_patch
"${CPMUTIL_PATCH_DIR}/${JSON_NAME}/${_patch}")
if(NOT EXISTS ${full_patch})
cpm_utils_message(FATAL_ERROR
"${JSON_NAME} specifies patch"
"${full_patch} which does not exist")
endif()
list(APPEND patches "${full_patch}")
endforeach()
endif()
# end format patchdir
# options
get_json_element("${object}" raw_options options)
unset(options)
if(raw_options)
array_to_list("${raw_options}" ${raw_options_LENGTH} options)
endif()
set(options ${options} ${JSON_OPTIONS})
# end options
# system/bundled
if(bundled STREQUAL "unset" AND DEFINED JSON_BUNDLED_PACKAGE)
set(bundled ${JSON_BUNDLED_PACKAGE})
endif()
endif()
endmacro()
# Get the JSON object for a key. Outputs to "object"
function(get_json_object key)
if(NOT DEFINED CPMFILE_CONTENT)
cpm_utils_message(FATAL_ERROR "${key}: No cpmfile present")
endif()
if(NOT DEFINED key)
cpm_utils_message(FATAL_ERROR "No JSON key specified")
endif()
string(JSON object ERROR_VARIABLE
err GET "${CPMFILE_CONTENT}" "${key}")
if(err)
cpm_utils_message(FATAL_ERROR "${key} not found in cpmfile\n"
"${err}")
endif()
Propagate(object)
endfunction()
# Get the download URL given the parameters.
# The preferred usage
function(AddJsonPackage)
set(oneValueArgs
NAME
# these are overrides that can be generated at runtime,
# so can be defined separately from the json
FORCE_BUNDLED_PACKAGE)
set(multiValueArgs OPTIONS)
set(optionArgs MODULE_PATH DOWNLOAD_ONLY)
cmake_parse_arguments(JSON
"${optionArgs}"
"${oneValueArgs}"
"${multiValueArgs}"
"${ARGN}")
list(LENGTH ARGN argnLength)
# single name argument
if(argnLength EQUAL 1)
set(JSON_NAME "${ARGV0}")
endif()
get_json_object("${JSON_NAME}")
parse_object(${object})
unset(EXTRA_ARGS)
if (JSON_MODULE_PATH)
list(APPEND EXTRA_ARGS MODULE_PATH)
endif()
if (JSON_DOWNLOAD_ONLY)
list(APPEND EXTRA_ARGS DOWNLOAD_ONLY)
endif()
if(ci)
AddCIPackage(
VERSION "${version}"
NAME "${name}"
REPO "${repo}"
PACKAGE "${package}"
EXTENSION "${extension}"
DISABLED_PLATFORMS "${disabled_platforms}"
GIT_HOST "${git_host}"
${EXTRA_ARGS})
else()
if (NOT DEFINED JSON_FORCE_BUNDLED_PACKAGE)
set(JSON_FORCE_BUNDLED_PACKAGE OFF)
endif()
AddPackage(
NAME "${package}"
VERSION "${version}"
MIN_VERSION "${min_version}"
URL "${url}"
HASH "${hash}"
REPO "${repo}"
PATCHES "${patches}"
OPTIONS "${options}"
FIND_PACKAGE_ARGUMENTS "${find_args}"
BUNDLED_PACKAGE "${bundled}"
FORCE_BUNDLED_PACKAGE "${JSON_FORCE_BUNDLED_PACKAGE}"
SOURCE_SUBDIR "${source_subdir}"
GIT_HOST "${git_host}"
ARTIFACT "${artifact}"
${EXTRA_ARGS})
endif()
# pass stuff to parent scope
Propagate(${package}_ADDED)
Propagate(${package}_SOURCE_DIR)
Propagate(${package}_BINARY_DIR)
Propagate(CMAKE_PREFIX_PATH)
endfunction()
# Internal function
function(AddPackage)
set(EXTRA_ARGS "")
set(oneValueArgs
NAME
MIN_VERSION
GIT_HOST
REPO
VERSION
ARTIFACT
HASH
URL
CUSTOM_KEY
SOURCE_SUBDIR
BUNDLED_PACKAGE
FORCE_BUNDLED_PACKAGE
FIND_PACKAGE_ARGUMENTS)
set(multiValueArgs OPTIONS PATCHES)
set(optionArgs MODULE_PATH DOWNLOAD_ONLY)
cmake_parse_arguments(PKG_ARGS
"${optionArgs}"
"${oneValueArgs}"
"${multiValueArgs}"
"${ARGN}")
if(NOT DEFINED PKG_ARGS_NAME)
cpm_utils_message(FATAL_ERROR "AddPackage: NAME is required")
endif()
if(NOT DEFINED PKG_ARGS_VERSION)
cpm_utils_message(FATAL_ERROR "${PKG_ARGS_NAME}: VERSION is required")
endif()
set(${PKG_ARGS_NAME}_CUSTOM_DIR "" CACHE STRING
"Path to a separately-downloaded copy of ${PKG_ARGS_NAME}")
option(${PKG_ARGS_NAME}_FORCE_SYSTEM
"Force the system package for ${PKG_ARGS_NAME}")
option(${PKG_ARGS_NAME}_FORCE_BUNDLED
"Force the bundled package for ${PKG_ARGS_NAME}")
# URL parsing
if (NOT DEFINED PKG_ARGS_URL)
get_package_url(URL_OUT "PKG_ARGS_URL"
GIT_URL_OUT "pkg_git_url"
GIT_HOST "${PKG_ARGS_GIT_HOST}"
REPO "${PKG_ARGS_REPO}"
VERSION "${PKG_ARGS_VERSION}"
ARTIFACT "${PKG_ARGS_ARTIFACT}"
PACKAGE "${PKG_ARGS_NAME}")
else()
set(pkg_git_url ${PKG_ARGS_URL})
endif()
cpm_utils_message(DEBUG "${PKG_ARGS_NAME} download URL is ${PKG_ARGS_URL}")
if(NOT DEFINED PKG_ARGS_HASH)
cpm_utils_message(FATAL_ERROR "${PKG_ARGS_NAME}: No hash defined")
endif()
#[[
Precedence:
- FORCE_BUNDLED_PACKAGE (caller override)
- package_FORCE_SYSTEM, package_FORCE_BUNDLED (user option)
- CPMUTIL_FORCE_SYSTEM, CPMUTIL_FORCE_BUNDLED (global option)
- BUNDLED_PACKAGE (json default)
- default to allow system
]]
if(PKG_ARGS_FORCE_BUNDLED_PACKAGE)
set(use_system OFF)
elseif(${PKG_ARGS_NAME}_FORCE_SYSTEM)
set(use_system ON)
set(force_system ON)
elseif(${PKG_ARGS_NAME}_FORCE_BUNDLED)
set(use_system OFF)
elseif(CPMUTIL_FORCE_SYSTEM)
set(use_system ON)
set(force_system ON)
elseif(CPMUTIL_FORCE_BUNDLED)
set(use_system OFF)
elseif(DEFINED PKG_ARGS_BUNDLED_PACKAGE AND
NOT PKG_ARGS_BUNDLED_PACKAGE STREQUAL "unset")
if(PKG_ARGS_BUNDLED_PACKAGE)
set(use_system OFF)
else()
set(use_system ON)
endif()
else()
set(use_system ON)
endif()
# find_package nonsense
if(use_system)
set(find_args ${PKG_ARGS_NAME})
if(DEFINED PKG_ARGS_MIN_VERSION)
list(APPEND find_args ${PKG_ARGS_MIN_VERSION})
endif()
if (DEFINED PKG_ARGS_FIND_PACKAGE_ARGUMENTS)
string(REPLACE " " ";"
passed_find_args
"${PKG_ARGS_FIND_PACKAGE_ARGUMENTS}")
set(find_args "${find_args};${passed_find_args}")
endif()
find_package(${find_args} QUIET)
if(${PKG_ARGS_NAME}_FOUND)
if(DEFINED ${PKG_ARGS_NAME}_VERSION)
set(sys_ver ${${PKG_ARGS_NAME}_VERSION})
else()
set(sys_ver ${PKG_ARGS_VERSION})
endif()
message(STATUS
"[CPMUtil] Using system package ${PKG_ARGS_NAME}@${sys_ver}")
CPMRegisterPackage(${PKG_ARGS_NAME} "${sys_ver}")
cpmutil_register_package(
${PKG_ARGS_NAME}
${pkg_git_url}
"${sys_ver} (system)")
set(${PKG_ARGS_NAME}_ADDED NO PARENT_SCOPE)
return()
endif()
if(force_system)
string(REPLACE ";" " " str_find_args "${find_args}")
message(FATAL_ERROR "[CPMUtil] ${PKG_ARGS_NAME} not found via "
"find_package(${str_find_args})")
endif()
endif()
# extra options
if (PKG_ARGS_OPTIONS)
list(APPEND EXTRA_ARGS OPTIONS "${PKG_ARGS_OPTIONS}")
endif()
if (PKG_ARGS_SOURCE_SUBDIR)
list(APPEND EXTRA_ARGS SOURCE_SUBDIR "${PKG_ARGS_SOURCE_SUBDIR}")
endif()
if (PKG_ARGS_DOWNLOAD_ONLY OR PKG_ARGS_MODULE_PATH)
list(APPEND EXTRA_ARGS DOWNLOAD_ONLY ON)
endif()
cpm_utils_message(STATUS
"Using bundled package ${PKG_ARGS_NAME}@${PKG_ARGS_VERSION}")
# Download/extract package
if (${PKG_ARGS_NAME}_CUSTOM_DIR STREQUAL "")
if (DEFINED PKG_ARGS_CUSTOM_KEY)
set(key ${PKG_ARGS_CUSTOM_KEY})
else()
set(key ${PKG_ARGS_VERSION})
endif()
# cache path
get_cache_path(${PKG_ARGS_NAME} ${key} cache_path)
fetch_package(
URL "${PKG_ARGS_URL}"
HASH "${PKG_ARGS_HASH}"
PATH "${cache_path}"
PATCHES "${PKG_ARGS_PATCHES}")
set(source_dir "${cache_path}")
else()
set(source_dir "${${PKG_ARGS_NAME}_CUSTOM_DIR}")
endif()
CPMAddPackage(
NAME ${PKG_ARGS_NAME}
VERSION ${PKG_ARGS_VERSION}
SOURCE_DIR ${source_dir}
EXCLUDE_FROM_ALL ON
${EXTRA_ARGS}
${PKG_ARGS_UNPARSED_ARGUMENTS})
cpmutil_register_package(
${PKG_ARGS_NAME}
${pkg_git_url}
${PKG_ARGS_VERSION})
set(${PKG_ARGS_NAME}_ADDED YES PARENT_SCOPE)
Propagate(${PKG_ARGS_NAME}_SOURCE_DIR)
Propagate(${PKG_ARGS_NAME}_BINARY_DIR)
if (PKG_ARGS_MODULE_PATH)
list(PREPEND CMAKE_PREFIX_PATH "${${ARTIFACT_PACKAGE}_SOURCE_DIR}")
Propagate(CMAKE_PREFIX_PATH)
endif()
endfunction()
# TODO(crueter): we could do an AddMultiArchPackage, multiplatformpackage?
# name is the artifact name, package is for find_package override
function(AddCIPackage)
set(oneValueArgs
VERSION
NAME
REPO
PACKAGE
EXTENSION
MIN_VERSION
GIT_HOST)
set(multiValueArgs DISABLED_PLATFORMS)
set(optionArgs MODULE_PATH)
cmake_parse_arguments(PKG_ARGS
"${optionArgs}"
"${oneValueArgs}"
"${multiValueArgs}"
${ARGN})
if(NOT DEFINED PKG_ARGS_VERSION)
cpm_utils_message(FATAL_ERROR "VERSION is required")
endif()
if(NOT DEFINED PKG_ARGS_NAME)
cpm_utils_message(FATAL_ERROR "NAME is required")
endif()
if(NOT DEFINED PKG_ARGS_REPO)
cpm_utils_message(FATAL_ERROR "REPO is required")
endif()
if(NOT DEFINED PKG_ARGS_PACKAGE)
cpm_utils_message(FATAL_ERROR "PACKAGE is required")
endif()
if(NOT DEFINED PKG_ARGS_EXTENSION)
set(ARTIFACT_EXT "tar.zst")
else()
set(ARTIFACT_EXT ${PKG_ARGS_EXTENSION})
endif()
if (NOT DEFINED PKG_ARGS_GIT_HOST)
set(ARTIFACT_GIT_HOST "github.com")
else()
set(ARTIFACT_GIT_HOST "${PKG_ARGS_GIT_HOST}")
endif()
if(DEFINED PKG_ARGS_DISABLED_PLATFORMS)
set(DISABLED_PLATFORMS ${PKG_ARGS_DISABLED_PLATFORMS})
endif()
# this is mildly annoying
set(ARTIFACT_VERSION ${PKG_ARGS_VERSION})
set(ARTIFACT_NAME ${PKG_ARGS_NAME})
set(ARTIFACT_REPO ${PKG_ARGS_REPO})
set(ARTIFACT_PACKAGE ${PKG_ARGS_PACKAGE})
# TODO: Use amd64/aarch64 naming for everything.
# Also drop macos universal
if (MSVC)
set(platname windows)
elseif(MINGW)
set(platname mingw)
elseif(ANDROID)
set(platname android)
elseif(LINUX)
set(platname linux)
elseif(IOS)
set(platname ios)
elseif(APPLE)
set(platname macos)
else()
cpm_utils_message(WARNING
"Unsupported platform ${CMAKE_SYSTEM_NAME} for CI packages")
endif()
if (APPLE AND NOT IOS)
set(archname universal)
elseif((WIN32 OR LINUX) AND CPMUTIL_AMD64)
set(archname amd64)
elseif(WIN32 AND CPMUTIL_ARM64)
set(archname arm64)
elseif((IOS OR LINUX OR ANDROID) AND CPMUTIL_ARM64)
set(archname aarch64)
elseif(ANDROID AND CPMUTIL_AMD64)
set(archname x86_64)
else()
cpm_utils_message(WARNING
"Unsupported platform/arch combo for CI packages")
endif()
if (DEFINED platname AND DEFINED archname)
set(pkgname ${platname}-${archname})
endif()
if (DEFINED pkgname
AND NOT "${pkgname}" IN_LIST DISABLED_PLATFORMS)
set(ARTIFACT
"${ARTIFACT_NAME}-${pkgname}-${ARTIFACT_VERSION}.${ARTIFACT_EXT}")
if (PKG_ARGS_MODULE_PATH)
set(EXTRA_ARGS MODULE_PATH)
endif()
# download sha512sum file
# TODO: CI pkgs
set(sha512sum_url
"https://${ARTIFACT_GIT_HOST}/${ARTIFACT_REPO}/releases/download/v${ARTIFACT_VERSION}/${ARTIFACT}.sha512sum")
set(sha512sum_file
"${CMAKE_CURRENT_BINARY_DIR}/.cpmutil_${ARTIFACT}_sha512sum")
file(DOWNLOAD "${sha512sum_url}" "${sha512sum_file}"
STATUS sha512sum_status)
list(GET sha512sum_status 0 sha512sum_error)
if(sha512sum_error)
message(FATAL_ERROR "[CPMUtil] Failed to download sha512sum "
"for ${ARTIFACT_NAME} from ${sha512sum_url}")
endif()
file(READ "${sha512sum_file}" sha512sum_hash)
string(STRIP "${sha512sum_hash}" sha512sum_hash)
file(REMOVE "${sha512sum_file}")
AddPackage(
NAME ${ARTIFACT_PACKAGE}
REPO ${ARTIFACT_REPO}
VERSION "v${ARTIFACT_VERSION}"
ARTIFACT ${ARTIFACT}
CUSTOM_KEY "${ARTIFACT_VERSION}-${pkgname}"
HASH ${sha512sum_hash}
FORCE_BUNDLED_PACKAGE ON
${EXTRA_ARGS})
set(${ARTIFACT_PACKAGE}_ADDED TRUE PARENT_SCOPE)
Propagate(${ARTIFACT_PACKAGE}_SOURCE_DIR)
Propagate(CMAKE_PREFIX_PATH)
else()
cpm_utils_message(FATAL_ERROR "${ARTIFACT_NAME}:"
"Unsupported platform ${pkgname} for CI package")
endif()
endfunction()
# Utility function for Qt
function(AddQt repo version)
if (NOT DEFINED repo)
message(FATAL_ERROR "[CPMUtil] AddQt: repo is required")
endif()
if (NOT DEFINED version)
message(FATAL_ERROR "[CPMUtil] AddQt: version is required")
endif()
AddCIPackage(
NAME qt
PACKAGE Qt6
VERSION ${version}
REPO ${repo}
DISABLED_PLATFORMS
android-x86_64 android-aarch64
MODULE_PATH)
find_package(Qt6 REQUIRED PATHS ${Qt6_SOURCE_DIR} NO_DEFAULT_PATH)
Propagate(CMAKE_PREFIX_PATH)
Propagate(Qt6_SOURCE_DIR)
endfunction()