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
curlis the command-line transfer tool.libcurlis 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
#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_inforeturns runtime library metadata without performing a network request.CURLVERSION_NOWasks for the current supported structure shape.The null check guards an unexpected unavailable result.
ssl_versionreports the active library’s TLS backend/version when present.The compile-time header version and runtime shared library must remain ABI-compatible.
cc -std=c17 -Wall -Wextra -Wpedantic -c curl_probe.cWithout 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.somessage 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
apt-cache policy libcurl4-openssl-dev
dpkg-query -W -f="${Status} ${Version}\n" libcurl4-openssl-dev 2>/dev/null || trueAPT 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-devas its OpenSSL-flavor development package.The old
libcurl4-nss-devadvice 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
sudo apt update
sudo apt install libcurl4-openssl-dev pkg-configAPT 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
dpkg -L libcurl4-openssl-dev | grep "/curl/curl.h$"
pkg-config --modversion libcurl
pkg-config --cflags --libs libcurl
curl-config --features 2>/dev/null || trueExpect 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-configdescribes the installation it belongs to; compare its prefix/version with pkg-config.The
SSLfeature means TLS support is compiled in; it does not prove certificate verification code is correct.
5. Compile and link using pkg-config
cc -std=c17 -Wall -Wextra -Wpedantic \
-o curl_probe curl_probe.c \
$(pkg-config --cflags --libs libcurl)
./curl_probeThe 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
/usrpaths.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
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
REQUIREDmakes 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 -S . -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build --verbose
./build/curl_probeCMake 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/localor 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
#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
.pcfiles cannot leak in.Use a CMake toolchain and root-path modes appropriate to the target.
Do not add
-I/usr/includeor-L/usr/libto 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
-lcurlalone may be insufficient for a static libcurl build.Use
pkg-config --static --libs libcurlor 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
pkg-config --modversion libcurl
ldd ./curl_probe | grep libcurl
./curl_probeCompare 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, andLD_LIBRARY_PATHcan override distro libraries.lddshould 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
Ubuntu libcurl4-openssl-dev documents the current Ubuntu 24.04 OpenSSL-flavor development package.
libcurl API overview explains easy/multi handles, initialization, cleanup, persistent connections, and build metadata.
libcurl programming tutorial documents compiler/link flags, features, transfers, and callbacks.
Using libcurl documents curl-config discovery commands.
CMake FindCURL documents
CURL::libcurland discovery variables.
Comments and corrections