From cb3bfd54f969ee754813d8b7db1012912a2e8b79 Mon Sep 17 00:00:00 2001 From: ClSlaid Date: Wed, 3 Sep 2025 22:05:45 +0800 Subject: [PATCH] [CI] Dowload prebuilt llvm in cmake (#192) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: 蔡略 Co-authored-by: ykiko --- .github/workflows/cmake.yml | 32 ++---- .gitignore | 9 +- CMakeLists.txt | 42 ++++++- cmake/github.cmake | 57 ++++++++++ cmake/llvm_setup.cmake | 213 ++++++++++++++++++++++++++++++++++++ cmake/utils.cmake | 55 ++++++++++ 6 files changed, 378 insertions(+), 30 deletions(-) create mode 100644 cmake/github.cmake create mode 100644 cmake/llvm_setup.cmake create mode 100644 cmake/utils.cmake diff --git a/.github/workflows/cmake.yml b/.github/workflows/cmake.yml index 67774949..4a52fab8 100644 --- a/.github/workflows/cmake.yml +++ b/.github/workflows/cmake.yml @@ -34,14 +34,16 @@ jobs: with: buckets: main apps: ninja - + - name: Setup llvm & libstdc++ & cmake & ninja if: matrix.os == 'ubuntu-24.04' run: | sudo apt update sudo apt install -y gcc-14 g++-14 libstdc++-14-dev + sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-14 100 sudo update-alternatives --install /usr/bin/g++ g++ /usr/bin/g++-14 100 + sudo update-alternatives --set gcc /usr/bin/gcc-14 sudo update-alternatives --set g++ /usr/bin/g++-14 @@ -59,24 +61,6 @@ jobs: - name: Checkout repository uses: actions/checkout@v4 - - name: Setup llvm binary - if: matrix.os == 'windows-2025' - run: | - curl -O -L "https://github.com/clice-project/llvm-binary/releases/download/20.1.5/x64-windows-msvc-release.7z" - 7z x x64-windows-msvc-release.7z "-o.llvm" - - - name: Setup llvm binary - if: matrix.os == 'ubuntu-24.04' - run: | - mkdir -p ./.llvm - curl -L "https://github.com/clice-project/llvm-binary/releases/download/20.1.5/x86_64-linux-gnu-debug.tar.xz" | tar -xJ -C ./.llvm - - - name: Setup llvm binary - if: matrix.os == 'macos-15' - run: | - mkdir -p ./.llvm - curl -L "https://github.com/clice-project/llvm-binary/releases/download/20.1.5/arm64-macosx-apple-debug.tar.xz" | tar -xJ -C ./.llvm - - name: Setup msvc sysroot for cmake if: matrix.os == 'windows-2025' uses: ilammy/msvc-dev-cmd@v1 @@ -84,20 +68,20 @@ jobs: - name: Build clice (release, windows) if: matrix.os == 'windows-2025' run: | - cmake -B build -G Ninja -DCMAKE_BUILD_TYPE=Release -DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++ -DLLVM_INSTALL_PATH="./.llvm" -DCLICE_ENABLE_TEST=ON + cmake -B build -G Ninja -DCMAKE_BUILD_TYPE=Release -DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++ -DCLICE_ENABLE_TEST=ON cmake --build build - name: Build clice (debug, linux) if: matrix.os == 'ubuntu-24.04' run: | - cmake -B build -G Ninja -DCMAKE_BUILD_TYPE=Debug -DCMAKE_C_COMPILER=clang-20 -DCMAKE_CXX_COMPILER=clang++-20 -DLLVM_INSTALL_PATH="./.llvm" -DCLICE_ENABLE_TEST=ON + cmake -B build -G Ninja -DCMAKE_BUILD_TYPE=Debug -DCMAKE_C_COMPILER=clang-20 -DCMAKE_CXX_COMPILER=clang++-20 -DCLICE_ENABLE_TEST=ON cmake --build build - name: Build clice (debug, macos) if: matrix.os == 'macos-15' run: | export PATH="/opt/homebrew/opt/llvm@20/bin:/opt/homebrew/opt/lld@20/bin:$PATH" - cmake -B build -G Ninja -DCMAKE_BUILD_TYPE=Debug -DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++ -DLLVM_INSTALL_PATH="./.llvm" -DCLICE_ENABLE_TEST=ON + cmake -B build -G Ninja -DCMAKE_BUILD_TYPE=Debug -DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++ -DCLICE_ENABLE_TEST=ON cmake --build build - name: Install uv for integration tests @@ -107,10 +91,10 @@ jobs: if: matrix.os == 'windows-2025' run: | ./build/bin/unit_tests.exe --test-dir="./tests/data" --resource-dir="./.llvm/lib/clang/20" - uv run pytest -s --log-cli-level=INFO tests/integration --executable=./build/bin/clice.exe --resource-dir="./.llvm/lib/clang/20" + uv run pytest -s --log-cli-level=INFO tests/integration --executable=./build/bin/clice.exe --resource-dir="./build/lib/clang/20" - name: Run tests if: matrix.os == 'ubuntu-24.04' || matrix.os == 'macos-15' run: | ./build/bin/unit_tests --test-dir="./tests/data" --resource-dir="./.llvm/lib/clang/20" - uv run pytest -s --log-cli-level=INFO tests/integration --executable=./build/bin/clice --resource-dir="./.llvm/lib/clang/20" + uv run pytest -s --log-cli-level=INFO tests/integration --executable=./build/bin/clice --resource-dir="./build/lib/clang/20" diff --git a/.gitignore b/.gitignore index 40326df1..97c205f4 100644 --- a/.gitignore +++ b/.gitignore @@ -31,13 +31,20 @@ *.out *.app +# Evil things +.claude + +# LSP & IDE +compile_commands.json +.vscode/ +.idea/ + temp/ *build*/ .cache/ .clice/ .llvm*/ .xmake/ -.vscode/ __pycache__/ .pytest_cache/ tests/unit/Local/ diff --git a/CMakeLists.txt b/CMakeLists.txt index 1711efb8..92e85c1d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -6,10 +6,10 @@ set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_EXPORT_COMPILE_COMMANDS ON) set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${PROJECT_BINARY_DIR}/bin) -# set llvm-libs install path -if(NOT DEFINED LLVM_INSTALL_PATH OR LLVM_INSTALL_PATH STREQUAL "") - message(FATAL_ERROR "Error: The variable LLVM_INSTALL_PATH is not set. Please specify it with -DLLVM_INSTALL_PATH=.") -endif() +include(cmake/llvm_setup.cmake) + +setup_llvm() + get_filename_component(LLVM_INSTALL_PATH "${LLVM_INSTALL_PATH}" ABSOLUTE) if(NOT EXISTS "${LLVM_INSTALL_PATH}") @@ -17,8 +17,15 @@ if(NOT EXISTS "${LLVM_INSTALL_PATH}") endif() message(STATUS "Found llvm-libs ${LLVM_INSTALL_PATH}") -set(CMAKE_PREFIX_PATH "${LLVM_INSTALL_PATH}") +if (EXISTS "${LLVM_CMAKE_DIR}") + list(APPEND CMAKE_MODULE_PATH "${LLVM_CMAKE_DIR}") +else() + list(APPEND CMAKE_PREFIX_PATH "${LLVM_INSTALL_PATH}") +endif() +message(STATUS "CMake module path: ${CMAKE_MODULE_PATH}") +message(STATUS "CMake prefix path: ${CMAKE_PREFIX_PATH}") +message(STATUS "Installing other dependencies, please wait...") # install dependencies include(FetchContent) if(CMAKE_SYSTEM_NAME STREQUAL "Linux" AND CMAKE_BUILD_TYPE STREQUAL "Debug") @@ -26,10 +33,13 @@ if(CMAKE_SYSTEM_NAME STREQUAL "Linux" AND CMAKE_BUILD_TYPE STREQUAL "Debug") set(ASAN ON CACHE BOOL "" FORCE) endif() +message(STATUS "Fetching tomlplusplus...") FetchContent_Declare( tomlplusplus GIT_REPOSITORY https://github.com/marzer/tomlplusplus.git ) + +message(STATUS "Fetching libuv...") FetchContent_Declare( libuv GIT_REPOSITORY https://github.com/libuv/libuv.git @@ -37,6 +47,9 @@ FetchContent_Declare( ) FetchContent_MakeAvailable(tomlplusplus libuv) +message(STATUS "Found tomlplusplus: ${tomlplusplus_SOURCE_DIR}") +message(STATUS "Found libuv: ${libuv_SOURCE_DIR}") + if(WIN32) set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreadedDLL") endif() @@ -85,11 +98,23 @@ add_library(clice-core STATIC "${CLICE_SOURCES}") # set llvm include and lib path add_library(llvm-libs INTERFACE IMPORTED) + + +message(STATUS "LLVM include path: ${LLVM_INSTALL_PATH}/include") +# add to include directories target_include_directories(llvm-libs INTERFACE "${LLVM_INSTALL_PATH}/include") + +if(EXISTS "${CMAKE_CURRENT_BINARY_DIR}/include") + # private files are downloaded here + message(STATUS "Fetched private headers to include: ${CMAKE_CURRENT_BINARY_DIR}/include") + target_include_directories(llvm-libs INTERFACE "${CMAKE_CURRENT_BINARY_DIR}/include") +endif() + if(WIN32) target_compile_definitions(llvm-libs INTERFACE "CLANG_BUILD_STATIC") target_link_libraries(llvm-libs INTERFACE version ntdll) endif() + if(CMAKE_BUILD_TYPE STREQUAL "Debug") target_link_directories(llvm-libs INTERFACE "${LLVM_INSTALL_PATH}/lib") target_link_libraries(llvm-libs INTERFACE @@ -130,6 +155,13 @@ target_link_libraries(clice PRIVATE clice-core) target_compile_options(clice PUBLIC ${CLICE_CXX_FLAGS}) target_link_options(clice PUBLIC ${CLICE_LINKER_FLAGS}) +# copy resource dir +message(STATUS "Copying resource directory") +file(COPY + ${LLVM_INSTALL_PATH}/lib/clang/20/ + DESTINATION ${PROJECT_BINARY_DIR}/lib/clang/20 +) + # clice tests if(CLICE_ENABLE_TEST) file(GLOB_RECURSE CLICE_TEST_SOURCES CONFIGURE_DEPENDS "${CMAKE_SOURCE_DIR}/tests/unit/*/*.cpp") diff --git a/cmake/github.cmake b/cmake/github.cmake new file mode 100644 index 00000000..861130ff --- /dev/null +++ b/cmake/github.cmake @@ -0,0 +1,57 @@ +include(${CMAKE_CURRENT_LIST_DIR}/utils.cmake) + +# Look up a Git tag's corresponding commit SHA from a GitHub repository +function(github_lookup_tag_commit REPO_OWNER REPO_NAME TAG_NAME OUTPUT_VAR) + set(GITHUB_API_URL "https://api.github.com/repos/${REPO_OWNER}/${REPO_NAME}/git/ref/tags/${TAG_NAME}") + + message(STATUS "Fetching tag info from: ${GITHUB_API_URL}") + + # Download tag info directly into a string + download_to_string(${GITHUB_API_URL} TAG_JSON DOWNLOAD_RESULT) + if(NOT DOWNLOAD_RESULT EQUAL "0") + message(WARNING "Failed to fetch tag info. Result: ${DOWNLOAD_RESULT}") + set(${OUTPUT_VAR} "NOTFOUND" PARENT_SCOPE) + return() + endif() + + # Parse the JSON to get object type and SHA + parse_json_field_from_string("${TAG_JSON}" object "type" OBJECT_TYPE) + parse_json_field_from_string("${TAG_JSON}" object "sha" OBJECT_SHA) + + if(OBJECT_TYPE STREQUAL "NOTFOUND" OR OBJECT_SHA STREQUAL "NOTFOUND") + message(WARNING "Could not find object type or SHA in the JSON response.") + set(${OUTPUT_VAR} "NOTFOUND" PARENT_SCOPE) + return() + endif() + + set(COMMIT_SHA "NOTFOUND") + + if(OBJECT_TYPE STREQUAL "commit") + # Direct commit reference + set(COMMIT_SHA ${OBJECT_SHA}) + elseif(OBJECT_TYPE STREQUAL "tag") + # Annotated tag - need to fetch the actual commit + set(TAG_API_URL "https://api.github.com/repos/${REPO_OWNER}/${REPO_NAME}/git/tags/${OBJECT_SHA}") + + message(STATUS "Fetching annotated tag info from: ${TAG_API_URL}") + + # Download annotated tag info directly into a string + download_to_string(${TAG_API_URL} ANNOTATED_TAG_JSON TAG_DOWNLOAD_RESULT) + if(NOT TAG_DOWNLOAD_RESULT EQUAL "0") + message(WARNING "Failed to fetch annotated tag info. Result: ${TAG_DOWNLOAD_RESULT}") + set(${OUTPUT_VAR} "NOTFOUND" PARENT_SCOPE) + return() + endif() + + # Parse the annotated tag JSON to get the commit SHA + parse_json_field_from_string("${ANNOTATED_TAG_JSON}" object "sha" COMMIT_SHA_FROM_TAG) + + if(NOT COMMIT_SHA_FROM_TAG STREQUAL "NOTFOUND") + set(COMMIT_SHA ${COMMIT_SHA_FROM_TAG}) + endif() + else() + message(WARNING "Unknown object type: ${OBJECT_TYPE}") + endif() + + set(${OUTPUT_VAR} ${COMMIT_SHA} PARENT_SCOPE) +endfunction() \ No newline at end of file diff --git a/cmake/llvm_setup.cmake b/cmake/llvm_setup.cmake new file mode 100644 index 00000000..bce0b835 --- /dev/null +++ b/cmake/llvm_setup.cmake @@ -0,0 +1,213 @@ +include_guard() + +include(${CMAKE_CURRENT_LIST_DIR}/github.cmake) + +# Check if LLVM version is supported +function(check_llvm_version llvm_ver OUTPUT_VAR) + if((NOT DEFINED llvm_ver) OR (llvm_ver STREQUAL "")) + message(WARNING "LLVM version is not set.") + set(${OUTPUT_VAR} FALSE PARENT_SCOPE) + elseif(NOT (llvm_ver VERSION_GREATER_EQUAL "20.1" AND llvm_ver VERSION_LESS "20.2")) + message(WARNING "Unsupported LLVM version: ${llvm_ver}. Only LLVM 20.1.x is supported.") + set(${OUTPUT_VAR} FALSE PARENT_SCOPE) + else() + set(${OUTPUT_VAR} TRUE PARENT_SCOPE) + endif() +endfunction() + +# Look up LLVM version's corresponding commit SHA +function(lookup_commit llvm_ver OUTPUT_VAR) + set(LLVM_TAG "llvmorg-${llvm_ver}") + github_lookup_tag_commit("llvm" "llvm-project" ${LLVM_TAG} COMMIT_SHA) + set(${OUTPUT_VAR} ${COMMIT_SHA} PARENT_SCOPE) +endfunction() + +# Fetch private Clang header files from LLVM source +function(fetch_private_clang_files llvm_ver) + set(PRIVATE_CLANG_FILE_LIST + "Sema/CoroutineStmtBuilder.h" + "Sema/TypeLocBuilder.h" + "Sema/TreeTransform.h" + ) + + # Check if all files already exist + set(PRIVATE_FILE_EXISTS TRUE) + foreach(FILE ${PRIVATE_CLANG_FILE_LIST}) + if(EXISTS "${LLVM_INSTALL_PATH}/include/clang/${FILE}") + message(STATUS "Private clang file found in LLVM installation: ${FILE}") + elseif(EXISTS "${CMAKE_CURRENT_BINARY_DIR}/include/clang/${FILE}") + message(STATUS "Private clang file already exists: ${FILE}") + else() + set(PRIVATE_FILE_EXISTS FALSE) + break() + endif() + endforeach() + + if(PRIVATE_FILE_EXISTS) + message(STATUS "All required private clang files already exist.") + return() + endif() + + # Skip download in offline build mode + if(CLICE_OFFLINE_BUILD) + message(WARNING "CLICE_OFFLINE_BUILD is enabled, skipping private clang files download") + message(WARNING "Build may fail if required private headers are missing") + return() + endif() + + message(WARNING "Required private clang files incomplete, fetching from llvm-project source...") + + # Get the commit SHA for this LLVM version + lookup_commit(${llvm_ver} LLVM_COMMIT) + if(LLVM_COMMIT STREQUAL "NOTFOUND") + message(WARNING "Failed to lookup commit for LLVM ${llvm_ver}, skipping private clang files download") + return() + endif() + + message(STATUS "LLVM ${llvm_ver} corresponds to commit ${LLVM_COMMIT}") + + # Download missing files + file(MAKE_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/include/clang") + foreach(FILE ${PRIVATE_CLANG_FILE_LIST}) + set(FILE_PATH "${CMAKE_CURRENT_BINARY_DIR}/include/clang/${FILE}") + if(NOT EXISTS "${FILE_PATH}") + message(STATUS "Downloading ${FILE}...") + file(DOWNLOAD "https://raw.githubusercontent.com/llvm/llvm-project/${LLVM_COMMIT}/clang/lib/${FILE}" + "${FILE_PATH}" + STATUS DOWNLOAD_STATUS + TLS_VERIFY ON) + list(GET DOWNLOAD_STATUS 0 STATUS_CODE) + if(NOT STATUS_CODE EQUAL 0) + message(FATAL_ERROR "Failed to download private clang file: ${FILE}") + endif() + endif() + endforeach() +endfunction() + +# Detect system-installed LLVM using llvm-config +function(detect_llvm OUTPUT_VAR) + find_program(LLVM_CONFIG_EXEC llvm-config) + + if(NOT LLVM_CONFIG_EXEC) + set(${OUTPUT_VAR} "" PARENT_SCOPE) + return() + endif() + + # Get LLVM version and paths + execute_process( + COMMAND "${LLVM_CONFIG_EXEC}" --version + OUTPUT_VARIABLE LLVM_VERSION + OUTPUT_STRIP_TRAILING_WHITESPACE + ) + execute_process( + COMMAND "${LLVM_CONFIG_EXEC}" --prefix + OUTPUT_VARIABLE LLVM_INSTALL_PATH_DETECTED + OUTPUT_STRIP_TRAILING_WHITESPACE + ) + execute_process( + COMMAND "${LLVM_CONFIG_EXEC}" --cmakedir + OUTPUT_VARIABLE LLVM_CMAKE_DIR_DETECTED + OUTPUT_STRIP_TRAILING_WHITESPACE + ) + + # Set cache variables + set(LLVM_INSTALL_PATH "${LLVM_INSTALL_PATH_DETECTED}" CACHE PATH "Path to LLVM installation" FORCE) + set(LLVM_CMAKE_DIR "${LLVM_CMAKE_DIR_DETECTED}" CACHE PATH "Path to LLVM CMake files" FORCE) + set(${OUTPUT_VAR} ${LLVM_VERSION} PARENT_SCOPE) +endfunction() + +# Download and install prebuilt LLVM binaries with error checking +function(install_prebuilt_llvm llvm_ver) + file(MAKE_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/.llvm") + + # Determine platform-specific package name + if(CMAKE_BUILD_TYPE STREQUAL "Debug") + set(LLVM_BUILD_TYPE "debug") + elseif(CMAKE_BUILD_TYPE STREQUAL "Release") + set(LLVM_BUILD_TYPE "release") + else() + set(LLVM_BUILD_TYPE "release-lto") + endif() + + if(WIN32) + set(LLVM_PACKAGE "x64-windows-msvc-${LLVM_BUILD_TYPE}.7z") + elseif(APPLE) + set(LLVM_PACKAGE "arm64-macosx-apple-${LLVM_BUILD_TYPE}.tar.xz") + elseif(UNIX) + set(LLVM_PACKAGE "x86_64-linux-gnu-${LLVM_BUILD_TYPE}.tar.xz") + endif() + + if(NOT LLVM_PACKAGE) + message(FATAL_ERROR "Unsupported platform or build type for prebuilt LLVM.") + endif() + + set(DOWNLOAD_PATH "${CMAKE_CURRENT_BINARY_DIR}/${LLVM_PACKAGE}") + + # Download if file does not exist + if(NOT EXISTS "${DOWNLOAD_PATH}") + message(STATUS "Downloading prebuilt LLVM package: ${LLVM_PACKAGE}") + set(DOWNLOAD_URL "https://github.com/clice-project/llvm-binary/releases/download/${llvm_ver}/${LLVM_PACKAGE}") + file(DOWNLOAD "${DOWNLOAD_URL}" + "${DOWNLOAD_PATH}" + STATUS DOWNLOAD_STATUS + SHOW_PROGRESS + TLS_VERIFY ON) + list(GET DOWNLOAD_STATUS 0 STATUS_CODE) + list(GET DOWNLOAD_STATUS 1 ERROR_MESSAGE) + + if(NOT STATUS_CODE EQUAL 0) + # Download failed, remove the incomplete file to force a fresh download next time + if(EXISTS "${DOWNLOAD_PATH}") + file(REMOVE "${DOWNLOAD_PATH}") + message(STATUS "Removed incomplete file: ${DOWNLOAD_PATH}") + endif() + message(FATAL_ERROR "Failed to download prebuilt LLVM package from ${DOWNLOAD_URL}.\nError: ${ERROR_MESSAGE}") + endif() + else() + message(STATUS "Prebuilt LLVM package already exists, skipping download.") + endif() + + message(STATUS "Extracting LLVM package: ${LLVM_PACKAGE}") + execute_process( + COMMAND "${CMAKE_COMMAND}" -E tar xvf "${DOWNLOAD_PATH}" + WORKING_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/.llvm" + RESULT_VARIABLE TAR_RESULT + OUTPUT_QUIET + ERROR_QUIET + ) + + if(NOT TAR_RESULT EQUAL "0") + message(FATAL_ERROR "Failed to extract archive. The file may be corrupted or the tool is missing.") + endif() + + # Set installation paths + set(LLVM_INSTALL_PATH "${CMAKE_CURRENT_BINARY_DIR}/.llvm" CACHE PATH "Path to LLVM installation" FORCE) + set(LLVM_CMAKE_DIR "${LLVM_INSTALL_PATH}/lib/cmake/llvm" CACHE PATH "Path to LLVM CMake files" FORCE) + message(STATUS "LLVM installation path set to: ${LLVM_INSTALL_PATH}") +endfunction() + +# Main function to set up LLVM for the project +function(setup_llvm) + # Use existing LLVM installation if path is already set + if(DEFINED LLVM_INSTALL_PATH AND NOT LLVM_INSTALL_PATH STREQUAL "") + message(STATUS "LLVM_INSTALL_PATH is set to ${LLVM_INSTALL_PATH}, using it directly.") + return() + endif() + + set(LLVM_VERSION_OK false) + if (CMAKE_BUILD_TYPE STREQUAL "Release") + # Try to detect system LLVM + detect_llvm(LLVM_VERSION) + check_llvm_version("${LLVM_VERSION}" LLVM_VERSION_OK) + endif() + + # Download prebuilt LLVM if system version is not suitable + if(NOT LLVM_VERSION_OK) + set(LLVM_VERSION "20.1.5") + message(WARNING "System LLVM not found, version mismatch or incompatible build type, downloading prebuilt LLVM...") + install_prebuilt_llvm("${LLVM_VERSION}") + endif() + + # Fetch required private Clang headers + fetch_private_clang_files("${LLVM_VERSION}") +endfunction() \ No newline at end of file diff --git a/cmake/utils.cmake b/cmake/utils.cmake new file mode 100644 index 00000000..bf168a5d --- /dev/null +++ b/cmake/utils.cmake @@ -0,0 +1,55 @@ +include_guard() + +# Download content from URL directly into a string. +# Usage: download_to_string(URL OUTPUT_VAR RESULT_VAR) +# - URL: The URL to download from. +# - OUTPUT_VAR: The variable to store the downloaded content. +# - RESULT_VAR: The variable to store the status code (0 on success). +function(download_to_string URL OUTPUT_VAR RESULT_VAR) + file(DOWNLOAD + ${URL} + ${CMAKE_CURRENT_BINARY_DIR}/temp_download_file + STATUS DOWNLOAD_STATUS + TLS_VERIFY ON + TIMEOUT 10 # Set a reasonable timeout + ) + + list(GET DOWNLOAD_STATUS 0 STATUS_CODE) + + if(NOT STATUS_CODE EQUAL 0) + list(GET DOWNLOAD_STATUS 1 ERROR_MSG) + message(WARNING "Failed to download from ${URL}: ${ERROR_MSG}") + set(${RESULT_VAR} ${STATUS_CODE} PARENT_SCOPE) + return() + endif() + + file(READ ${CMAKE_CURRENT_BINARY_DIR}/temp_download_file DOWNLOADED_CONTENT) + file(REMOVE ${CMAKE_CURRENT_BINARY_DIR}/temp_download_file) + + set(${OUTPUT_VAR} ${DOWNLOADED_CONTENT} PARENT_SCOPE) + set(${RESULT_VAR} 0 PARENT_SCOPE) +endfunction() + +# Parse a nested JSON field from a string. +# Usage: parse_json_field_from_string(JSON_STRING PARENT_KEY FIELD_NAME OUTPUT_VAR) +# - JSON_STRING: A variable containing the JSON content as a string. +# - PARENT_KEY: A key string to get from the JSON content. +# - FIELD_NAME: The field name to extract. +# - OUTPUT_VAR: The variable to store the extracted value. +function(parse_json_field_from_string JSON_CONTENT) + # Get the last argument as the output variable name + set(ARGS "${ARGN}") + list(GET ARGS -1 OUTPUT_VAR) + list(REMOVE_AT ARGS -1) + + string(JSON FIELD_VALUE GET "${JSON_CONTENT}" ${ARGS}) + + if(NOT FIELD_VALUE) + string(JOIN " " FIELD_PATH_STR ${ARGS}) + message(WARNING "Could not parse field '${FIELD_PATH_STR}' from JSON.") + set(${OUTPUT_VAR} "NOTFOUND" PARENT_SCOPE) + return() + endif() + + set(${OUTPUT_VAR} ${FIELD_VALUE} PARENT_SCOPE) +endfunction() \ No newline at end of file