Table of Contents

  1. Finding libcurl
  2. TLS Backend Selection
  3. Protocol Feature Detection
  4. Static vs Shared Linking
  5. Building from Source
  6. CURL with OpenSSL
  7. Testing HTTP Endpoints in CTest
Back to CMake Mastery Series

CURL

June 4, 2026 Wasil Zafar 8 min read

The complete guide to integrating libcurl with CMake — from basic discovery and TLS backend selection to protocol detection, static linking, and automated HTTP testing with CTest.

Networking

Finding libcurl

libcurl is the multiprotocol file transfer library used by virtually every networked C/C++ application. CMake provides a first-class FindCURL module that discovers system-installed libcurl and exposes the modern CURL::libcurl imported target.

# CMakeLists.txt — Basic libcurl integration
cmake_minimum_required(VERSION 3.20)
project(HttpClient LANGUAGES CXX)

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

# Find system libcurl
find_package(CURL REQUIRED)

add_executable(http_client src/main.cpp src/downloader.cpp)
target_link_libraries(http_client PRIVATE CURL::libcurl)

# Print discovered details
message(STATUS "CURL found: ${CURL_FOUND}")
message(STATUS "CURL version: ${CURL_VERSION_STRING}")
message(STATUS "CURL include: ${CURL_INCLUDE_DIRS}")
message(STATUS "CURL libraries: ${CURL_LIBRARIES}")
Key Insight: Always prefer the CURL::libcurl imported target over the legacy ${CURL_LIBRARIES} variable. The target automatically propagates include directories, compile definitions, and handles transitive dependencies — especially important when curl links against OpenSSL or zlib.

For specifying a minimum version requirement:

# Require specific minimum version
find_package(CURL 7.80 REQUIRED)

# Or use version range (CMake 3.19+)
find_package(CURL 7.80...8.99 REQUIRED)

TLS Backend Selection

libcurl supports multiple TLS backends — OpenSSL, wolfSSL, mbedTLS, Schannel (Windows), and Secure Transport (macOS). The backend determines certificate handling, cipher support, and licensing implications.

# Detect which TLS backend curl was built with
find_package(CURL REQUIRED)

# Check via curl-config or pkg-config
execute_process(
    COMMAND curl-config --ssl-backends
    OUTPUT_VARIABLE CURL_SSL_BACKENDS
    OUTPUT_STRIP_TRAILING_WHITESPACE
    ERROR_QUIET
)
message(STATUS "CURL TLS backends: ${CURL_SSL_BACKENDS}")

# Verify OpenSSL is the TLS backend
if(CURL_SSL_BACKENDS MATCHES "OpenSSL")
    message(STATUS "CURL uses OpenSSL — LGPL/Apache-2.0 compatible")
elseif(CURL_SSL_BACKENDS MATCHES "Schannel")
    message(STATUS "CURL uses Windows Schannel — no extra dependencies")
elseif(CURL_SSL_BACKENDS MATCHES "SecureTransport")
    message(STATUS "CURL uses macOS Secure Transport")
endif()
// src/tls_check.cpp — Runtime TLS backend verification
#include <curl/curl.h>
#include <iostream>

void printTlsInfo() {
    curl_version_info_data* info = curl_version_info(CURLVERSION_NOW);

    std::cout << "libcurl version: " << info->version << "\n";
    std::cout << "SSL version: " << (info->ssl_version ? info->ssl_version : "none") << "\n";
    std::cout << "Protocols: ";
    for (const char* const* p = info->protocols; *p; ++p) {
        std::cout << *p << " ";
    }
    std::cout << "\n";
}

Protocol Feature Detection

Not all curl builds support all protocols. CMake can detect available protocols at configure time and conditionally compile features.

# CMakeLists.txt — Protocol feature detection
find_package(CURL REQUIRED)

# Check for specific protocol support
include(CheckCSourceRuns)
set(CMAKE_REQUIRED_LIBRARIES CURL::libcurl)

# Test HTTPS support
check_c_source_runs("
#include <curl/curl.h>
int main() {
    curl_version_info_data* info = curl_version_info(CURLVERSION_NOW);
    return (info->features & CURL_VERSION_SSL) ? 0 : 1;
}
" CURL_HAS_SSL)

# Test HTTP/2 support
check_c_source_runs("
#include <curl/curl.h>
int main() {
    curl_version_info_data* info = curl_version_info(CURLVERSION_NOW);
    return (info->features & CURL_VERSION_HTTP2) ? 0 : 1;
}
" CURL_HAS_HTTP2)

if(CURL_HAS_SSL)
    target_compile_definitions(http_client PRIVATE HAS_HTTPS=1)
endif()

if(CURL_HAS_HTTP2)
    target_compile_definitions(http_client PRIVATE HAS_HTTP2=1)
endif()
Pitfall: Minimal curl builds (e.g., Alpine Linux's libcurl-minimal) may lack HTTPS, HTTP/2, or even FTP support. Always detect features at configure time rather than assuming availability — especially in containerized CI environments.

Static vs Shared Linking

Linking libcurl statically bundles it into your binary, eliminating runtime dependencies but requiring you to also link curl's own dependencies (OpenSSL, zlib, etc.).

# CMakeLists.txt — Static curl linking
option(CURL_USE_STATIC "Link libcurl statically" OFF)

if(CURL_USE_STATIC)
    # Tell FindCURL to prefer static libraries
    set(CURL_USE_STATIC_LIBS ON)

    # Static curl requires CURL_STATICLIB define
    find_package(CURL REQUIRED)
    target_compile_definitions(http_client PRIVATE CURL_STATICLIB)

    # Static curl needs its dependencies explicitly
    find_package(OpenSSL REQUIRED)
    find_package(ZLIB REQUIRED)

    target_link_libraries(http_client PRIVATE
        CURL::libcurl
        OpenSSL::SSL
        OpenSSL::Crypto
        ZLIB::ZLIB
    )

    # Platform-specific socket libraries
    if(WIN32)
        target_link_libraries(http_client PRIVATE ws2_32 crypt32 wldap32)
    endif()
else()
    find_package(CURL REQUIRED)
    target_link_libraries(http_client PRIVATE CURL::libcurl)
endif()

Building from Source

When system curl is too old or you need specific features, build curl from source using FetchContent or ExternalProject:

# CMakeLists.txt — Build curl from source via FetchContent
cmake_minimum_required(VERSION 3.20)
project(HttpClient LANGUAGES C CXX)

include(FetchContent)

# Disable curl features we don't need
set(BUILD_CURL_EXE OFF CACHE BOOL "" FORCE)
set(BUILD_SHARED_LIBS OFF CACHE BOOL "" FORCE)
set(CURL_DISABLE_LDAP ON CACHE BOOL "" FORCE)
set(CURL_DISABLE_LDAPS ON CACHE BOOL "" FORCE)
set(CURL_DISABLE_TELNET ON CACHE BOOL "" FORCE)
set(CURL_DISABLE_DICT ON CACHE BOOL "" FORCE)
set(CURL_DISABLE_FILE ON CACHE BOOL "" FORCE)
set(CURL_DISABLE_TFTP ON CACHE BOOL "" FORCE)
set(CURL_DISABLE_RTSP ON CACHE BOOL "" FORCE)
set(CURL_DISABLE_POP3 ON CACHE BOOL "" FORCE)
set(CURL_DISABLE_IMAP ON CACHE BOOL "" FORCE)
set(CURL_DISABLE_SMTP ON CACHE BOOL "" FORCE)
set(CURL_DISABLE_GOPHER ON CACHE BOOL "" FORCE)
set(HTTP_ONLY ON CACHE BOOL "" FORCE)

FetchContent_Declare(curl
    GIT_REPOSITORY https://github.com/curl/curl.git
    GIT_TAG        curl-8_7_1
)
FetchContent_MakeAvailable(curl)

add_executable(http_client src/main.cpp)
target_link_libraries(http_client PRIVATE libcurl_static)
target_compile_definitions(http_client PRIVATE CURL_STATICLIB)
Key Insight: Disabling unused protocols (LDAP, Telnet, DICT, etc.) dramatically reduces build time and binary size. The HTTP_ONLY flag is the most aggressive — it builds curl with only HTTP/HTTPS support.

CURL with OpenSSL

When building curl from source, you can explicitly specify OpenSSL as the TLS backend and control certificate bundle paths:

# Build curl with specific OpenSSL version
find_package(OpenSSL 3.0 REQUIRED)

set(CURL_USE_OPENSSL ON CACHE BOOL "" FORCE)
set(CURL_CA_BUNDLE "/etc/ssl/certs/ca-certificates.crt" CACHE STRING "" FORCE)
set(CURL_CA_PATH "/etc/ssl/certs" CACHE STRING "" FORCE)

FetchContent_Declare(curl
    GIT_REPOSITORY https://github.com/curl/curl.git
    GIT_TAG        curl-8_7_1
)
FetchContent_MakeAvailable(curl)
// src/https_client.cpp — HTTPS request with certificate verification
#include <curl/curl.h>
#include <string>
#include <iostream>

static size_t writeCallback(void* contents, size_t size, size_t nmemb, std::string* output) {
    size_t totalSize = size * nmemb;
    output->append(static_cast<char*>(contents), totalSize);
    return totalSize;
}

int main() {
    curl_global_init(CURL_GLOBAL_DEFAULT);
    CURL* curl = curl_easy_init();

    if (curl) {
        std::string response;

        curl_easy_setopt(curl, CURLOPT_URL, "https://api.github.com/zen");
        curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writeCallback);
        curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response);
        curl_easy_setopt(curl, CURLOPT_USERAGENT, "CMake-Demo/1.0");

        // TLS verification (always enable in production)
        curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 1L);
        curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 2L);

        CURLcode res = curl_easy_perform(curl);
        if (res == CURLE_OK) {
            std::cout << "Response: " << response << "\n";
        } else {
            std::cerr << "Error: " << curl_easy_strerror(res) << "\n";
        }

        curl_easy_cleanup(curl);
    }

    curl_global_cleanup();
    return 0;
}

Testing HTTP Endpoints in CTest

CTest can validate that HTTP services respond correctly. Combine curl's CLI with add_test for integration testing:

# CMakeLists.txt — HTTP integration tests
enable_testing()

# Find curl CLI for integration tests
find_program(CURL_EXECUTABLE curl REQUIRED)

# Test that our server starts and responds
add_test(
    NAME integration_health_check
    COMMAND ${CURL_EXECUTABLE}
        --silent --fail
        --max-time 5
        --retry 3
        --retry-delay 1
        http://localhost:8080/health
)
set_tests_properties(integration_health_check PROPERTIES
    LABELS "integration"
    TIMEOUT 30
    FIXTURES_REQUIRED server_running
)

# Test JSON API endpoint
add_test(
    NAME integration_api_users
    COMMAND ${CURL_EXECUTABLE}
        --silent --fail
        --header "Content-Type: application/json"
        --header "Accept: application/json"
        --max-time 10
        http://localhost:8080/api/users
)
set_tests_properties(integration_api_users PROPERTIES
    LABELS "integration"
    TIMEOUT 30
)

# Test POST request
add_test(
    NAME integration_api_create
    COMMAND ${CURL_EXECUTABLE}
        --silent --fail
        --request POST
        --header "Content-Type: application/json"
        --data "{\"name\":\"test\",\"email\":\"test@example.com\"}"
        --max-time 10
        http://localhost:8080/api/users
)

# Run integration tests separately
# cmake --build . && ctest -L integration
Key Insight: Use CTest's FIXTURES_REQUIRED and FIXTURES_SETUP properties to ensure your server starts before HTTP tests run. Combine with --retry flags on the curl command to handle startup latency gracefully.