Seeing curl work in a terminal while #include <curl/curl.h> fails is not contradictory. The command-line client, runtime shared library, and development interface are separate packages. On current Ubuntu LTS, the usual native-build fix is libcurl4-openssl-dev; the old NSS-flavor recommendation belongs to an earlier packaging era.

curl and libcurl are different products

  • curl is the command-line transfer tool.

  • libcurl is the reusable client-side URL transfer library.

  • The runtime package lets existing programs load libcurl.

  • The development package supplies curl/curl.h, link-time files, and metadata needed to build applications.

  • A libcurl build may use OpenSSL, GnuTLS, rustls, wolfSSL, or another supported TLS backend; application code should use libcurl’s public API rather than backend internals.

1. Reproduce with a safe version probe

curl_probe.cc
#include <curl/curl.h>
#include <stdio.h>
 
int main(void)
{
    const curl_version_info_data *info =
        curl_version_info(CURLVERSION_NOW);
 
    if (info == NULL) {
        return 1;
    }
 
    printf("libcurl %s\n", info->version);
    printf("TLS backend: %s\n",
           info->ssl_version != NULL ? info->ssl_version : "none");
    return 0;
}

This checks the library you actually loaded

  • curl_version_info returns runtime library metadata without performing a network request.

  • CURLVERSION_NOW asks for the current supported structure shape.

  • The null check guards an unexpected unavailable result.

  • ssl_version reports the active library’s TLS backend/version when present.

  • The compile-time header version and runtime shared library must remain ABI-compatible.

Directory containing curl_probe.cbash
cc -std=c17 -Wall -Wextra -Wpedantic -c curl_probe.c
Without development headers in the compiler search path, compilation stops at curl/curl.h with “No such file or directory.”

Identify the failing build stage

  • A missing header is a preprocessor/compiler discovery problem.

  • An undefined reference to curl_* is a linker problem.

  • A missing libcurl.so message is a runtime loader/ABI problem.

  • A CURLE error is an application/protocol/runtime problem after a successful build.

  • Fix the first failed stage rather than mixing unrelated flags.

2. Inspect Ubuntu package state

Ubuntu shellbash
apt-cache policy libcurl4-openssl-dev
dpkg-query -W -f="${Status} ${Version}\n" libcurl4-openssl-dev 2>/dev/null || true
APT prints the repository candidate; dpkg prints installed status and version when the development package is present.

The current default is the OpenSSL flavor

  • Ubuntu 24.04 publishes libcurl4-openssl-dev as its OpenSSL-flavor development package.

  • The old libcurl4-nss-dev advice should not be copied into a new native setup.

  • Development flavor packages can conflict/replace one another because they provide the same public libcurl development interface.

  • Choose the distro-supported flavor required by the product and dependency policy.

  • Package versions differ across Ubuntu releases and update/security pockets.

3. Install the headers and build metadata

Ubuntu development hostbash
sudo apt update
sudo apt install libcurl4-openssl-dev pkg-config
APT refreshes repository metadata and proposes the libcurl development package, matching runtime dependencies, and pkg-config tooling.

Risk level: caution. Review the command before running it.

Review package changes before confirmation

  • These commands require administrative authorization and change the host.

  • APT resolves a runtime library version compatible with the selected development package.

  • A different installed libcurl development flavor may be removed/replaced; read the transaction plan.

  • Use a pinned development container/image in CI for reproducible toolchains.

  • Rebuild and test after security upgrades according to the application release policy.

4. Verify files, metadata, and features

Ubuntu shellbash
dpkg -L libcurl4-openssl-dev | grep "/curl/curl.h$"
pkg-config --modversion libcurl
pkg-config --cflags --libs libcurl
curl-config --features 2>/dev/null || true
Expect the packaged header path, libcurl version, build/link flags, and—when curl-config is installed—compiled feature names such as SSL.

Metadata knows more than -lcurl

  • The header may live in a multiarch include directory rather than the path you guessed.

  • pkg-config can emit include/library flags and private dependencies required by a selected/static build.

  • Empty include flags are valid when the header directory is already a compiler default.

  • curl-config describes the installation it belongs to; compare its prefix/version with pkg-config.

  • The SSL feature means TLS support is compiled in; it does not prove certificate verification code is correct.

Directory containing curl_probe.cbash
cc -std=c17 -Wall -Wextra -Wpedantic \
  -o curl_probe curl_probe.c \
  $(pkg-config --cflags --libs libcurl)
./curl_probe
The executable prints the loader-selected libcurl version and TLS backend.

Source first, libraries after

  • The source/object appears before libraries, which helps linkers resolving symbols left to right.

  • pkg-config supplies the installed libcurl interface instead of hard-coded /usr paths.

  • Command substitution must come from trusted local metadata; do not let untrusted environment values control privileged builds.

  • A successful probe confirms build/loading, not network policy or application correctness.

  • Record the resolved flags/version in CI artifacts when diagnosing environment drift.

6. Use CMake imported targets

CMakeLists.txtcmake
cmake_minimum_required(VERSION 3.20)
project(curl_probe LANGUAGES C)
 
find_package(CURL REQUIRED)
 
add_executable(curl_probe curl_probe.c)
target_compile_features(curl_probe PRIVATE c_std_17)
target_link_libraries(curl_probe PRIVATE CURL::libcurl)

CURL::libcurl carries usage requirements

  • REQUIRED makes configuration fail clearly when development files are absent.

  • The imported target carries include directories and link information.

  • Target-scoped linking avoids global include/link flags.

  • Request/version-check libcurl according to the APIs/features the source actually needs.

  • Use a fresh build directory after switching libcurl installations or toolchains.

CMake project rootbash
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build --verbose
./build/curl_probe
CMake reports the selected CURL installation, compiles the target, and the probe prints runtime details.

Verbose builds expose accidental selection

  • Inspect actual -I, -L, library path, and target architecture.

  • CMake cache may retain an old /usr/local or vendored installation.

  • Do not edit generated cache entries blindly; recreate the build directory or pass intentional toolchain hints.

  • Cross-compiling requires a target sysroot/toolchain, not host package discovery.

  • Runtime loader selection can still differ from the link path.

A minimal HTTPS transfer that keeps verification on

https_fetch.cc
#include <curl/curl.h>
#include <stdio.h>
 
int main(void)
{
    CURL *handle = NULL;
    CURLcode code;
 
    code = curl_global_init(CURL_GLOBAL_DEFAULT);
    if (code != CURLE_OK) return 1;
 
    handle = curl_easy_init();
    if (handle == NULL) {
        curl_global_cleanup();
        return 1;
    }
 
    curl_easy_setopt(handle, CURLOPT_URL, "https://example.com/");
    curl_easy_setopt(handle, CURLOPT_FOLLOWLOCATION, 1L);
    curl_easy_setopt(handle, CURLOPT_MAXREDIRS, 5L);
    curl_easy_setopt(handle, CURLOPT_TIMEOUT, 15L);
 
    code = curl_easy_perform(handle);
    if (code != CURLE_OK) {
        fprintf(stderr, "transfer failed: %s\n",
                curl_easy_strerror(code));
    }
 
    curl_easy_cleanup(handle);
    curl_global_cleanup();
    return code == CURLE_OK ? 0 : 1;
}

Do not disable TLS verification to make a test pass

  • HTTPS peer and hostname verification remain enabled by default.

  • The easy handle owns transfer state and is cleaned on every initialized path.

  • Global init/cleanup occur once around libcurl use; threaded programs must follow current thread-safety guidance.

  • Redirect and overall timeout limits prevent unbounded behavior.

  • Production clients need response callbacks/limits, protocol restrictions, proxy policy, cancellation, retry semantics, secrets protection, and certificate trust management.

When the package is installed but the header is unseen

  • The compiler runs inside another container/chroot/remote builder.

  • -nostdinc, a custom sysroot, or toolchain file excludes host directories.

  • The target architecture needs its own development package/sysroot.

  • CMake/pkg-config/curl-config selects a stale custom prefix.

  • CPATH, PKG_CONFIG_PATH, PKG_CONFIG_LIBDIR, or cache values point to a conflicting installation.

  • A Snap/IDE sandbox cannot see the host package.

  • The command uses a different compiler than the one tested interactively.

Cross-compilation rules

  • Use target headers and target libcurl libraries from the same sysroot.

  • Configure pkg-config sysroot/library directories so host .pc files cannot leak in.

  • Use a CMake toolchain and root-path modes appropriate to the target.

  • Do not add -I/usr/include or -L/usr/lib to an ARM/embedded build.

  • Confirm the target libcurl feature/TLS backend set because it may differ from Ubuntu host libcurl.

  • Run on target/emulator and inspect the produced binary architecture/dependencies.

Static linking needs the complete graph

  • -lcurl alone may be insufficient for a static libcurl build.

  • Use pkg-config --static --libs libcurl or the build system’s exported metadata.

  • Static dependencies can include TLS, compression, IDN, PSL, SSH, HTTP/2/3, resolver, and platform libraries.

  • Static linking changes security-update/rebuild and license-compliance responsibilities.

  • Do not combine headers from one libcurl with a static archive from another prefix/version.

Check build and runtime selection

Built executable directorybash
pkg-config --modversion libcurl
ldd ./curl_probe | grep libcurl
./curl_probe
Compare build metadata with the shared object chosen by the runtime loader and the version/backend reported by libcurl.

Multiple installations can split the truth

  • /usr/local, custom RPATH/RUNPATH, loader cache, and LD_LIBRARY_PATH can override distro libraries.

  • ldd should only inspect trusted binaries.

  • The curl CLI may be linked to a different libcurl than your application.

  • Remove unintended shadow installations or isolate intentional prefixes consistently.

  • Never copy arbitrary shared objects into system directories to silence loader failures.

Common failures decoded

  • curl/curl.h missing: development headers absent or excluded by compiler/sysroot.

  • undefined reference to curl_easy_*: libcurl not linked, wrong order, or metadata missing.

  • CMake cannot find CURL: package absent, stale cache, wrong prefix/toolchain/sysroot.

  • Works locally, fails in CI: CI image lacks the dev package or selects a different libcurl/toolchain.

  • HTTPS unsupported: target libcurl was built without an SSL feature/backend.

  • certificate verify failed: CA chain/path, hostname, time, proxy interception, or server configuration is wrong—do not disable verification.

  • runtime libcurl missing/wrong: loader paths or ABI package differ from build environment.

  • NSS package conflict: obsolete flavor advice is replacing the supported development variant; use the product/distro-approved backend.

Verification checklist

  • The failure stage is identified: include, link, load, feature, or transfer.

  • Ubuntu-supported target development package is installed from an approved repository.

  • dpkg and pkg-config/CMake resolve the intended headers and libraries.

  • Native, clean CI/container, and target cross builds use metadata rather than hard-coded paths.

  • Runtime libcurl version, TLS backend/features, CA trust, redirects, protocol restrictions, timeouts, and response limits are tested.

  • Security update ownership, static/dynamic rebuild policy, dependency licenses/SBOM, and supported versions are documented.

Primary references