38.3 Függőségek, package discovery és linkhatárok

A függőségkezelés a natív build egyik legkönnyebben elrontott része. Nem elég azt elérni, hogy a fordítás és a linkelés egyszer működjön: a függőség verziója, bináris formája, tranzitív linkigénye, ABI-feltétele és jogi információja is része a build szerződésének. Ez a rész a Modern CMake target-alapú projekt (38.2) target-fogalmára épül, és a bináris határ kérdéseit a Stabil shared library határ és verziózás (36.3) fogalmaival köti össze. A cél nem egy univerzális csomagkezelő kiválasztása, hanem annak eldöntése, hogy egy adott függőséget hogyan lehet reprodukálhatóan megtalálni, linkelni és frissíteni.

38.3.1 A függőségi szerződés szintjei

Függőség (dependency) alatt itt minden olyan artefaktumot értünk, amelyre a projekt buildje vagy futása támaszkodik: fejléc, statikus vagy shared library, generált fájl, esetleg egy teljes eszközlánc. A discovery annak mechanizmusa, ahogy a build rendszer megkeresi a függőség fejléceit, linkelhető binárisát és használati követelményeit. Az imported target olyan CMake-target, amelyet nem ez a projekt épít, hanem egy find_package vagy egy modul ír le; tulajdonságai között szerepelhet az include-könyvtár, a linkelendő library, a szükséges nyelvi szint és a tranzitív linkigény.

A package config a függőség által szállított CMake-fájl, jellemzően <Pkg>Config.cmake és <Pkg>ConfigVersion.cmake, amelyet a find_package(<Pkg> CONFIG) keres. A pkg-config ezzel szemben .pc fájlokon alapuló, több nyelv által használt metadata-lekérdezés; a CMake PkgConfig modulja ezt fordítja targetté. Az ABI (application binary interface) a bináris kompatibilitás feltételrendszere: hívási konvenció, névtorzítás, objektumlayout, kivételkezelés, allokátor és standard library viselkedése. A tranzitív link azt jelenti, hogy egy library linkelési igénye továbbgyűrűzik a fogyasztóhoz; ez static és shared esetben eltérően viselkedik.

38.3.2 find_package és imported target

A find_package két fő módja a MODULE és a CONFIG. A MODULE mód a projekt saját Find<Package>.cmake fájlját futtatja, ami hasznos régi vagy nem CMake-alapú függőségeknél, de gyakran kézzel karbantartott, pontatlan felderítést ad. Modern függőségeknél a CONFIG mód a jobb választás, mert a csomag saját, telepített metadata-fájljait használja:

find_package(fmt CONFIG REQUIRED)
add_executable(app src/main.cpp)
target_link_libraries(app PRIVATE fmt::fmt)

A fmt::fmt név konvenció: a névtér a csomag neve, a target neve a tényleges library. A verzió megadható például find_package(fmt 10 CONFIG REQUIRED) formában; a pontos verziószám itt csak szemléltetés, a konkrét projektnél a ténylegesen támogatott verziót kell rögzíteni. A verziókompatibilitást a csomag ConfigVersion.cmake fájlja dönti el; gyakori stratégiák az AnyNewerVersion, a SameMajorVersion, a SameMinorVersion és az ExactVersion. A SameMajorVersion például azt jelenti, hogy egy 1.x függőséghez a fogyasztó 1.y-t is elfogad, de 2.0-t nem.

Az imported target előnye, hogy a használati követelmények egy helyen vannak. Ha a fogyasztó kézzel írja be az include-könyvtárat és a -l kapcsolót, elveszíti a fordítási definíciókat, a szükséges nyelvi szintet, a tranzitív linkigényeket és a konfigurációspecifikus útvonalakat. Ezért a cél: target_link_libraries egy imported targettel, ne include_directories és link_directories kombinációja.

38.3.3 pkg-config és CMake package config

A pkg-config akkor hasznos, ha a függőség nem szállít CMake package configot, de szállít .pc fájlt. A CMake-oldali használat:

find_package(PkgConfig REQUIRED)
pkg_check_modules(TEXTUTIL REQUIRED IMPORTED_TARGET textutil)
target_link_libraries(app PRIVATE PkgConfig::TEXTUTIL)

Az IMPORTED_TARGET kulcs fontos: enélkül csak változókat kapunk (TEXTUTIL_CFLAGS, TEXTUTIL_LIBS), amelyeket kézzel kellene összefűzni, és elveszne a target-alapú következetesség. A .pc fájl jellemzően prefix, includedir, libdir, Cflags és Libs mezőket tartalmaz; ezek a telepítési prefixhez kötődnek, ezért a .pc fájl áthelyezése vagy a prefix megváltoztatása könnyen hibás útvonalat eredményez.

A CMake package config erősebb szerződés, mert nem csak kapcsolókat, hanem targeteket, tulajdonságokat és verziókompatibilitást is leír. Hátránya, hogy a csomagnak telepítenie kell a config fájlt, és a fogyasztónak a keresési útvonalat (CMAKE_PREFIX_PATH, CMAKE_FIND_ROOT_PATH) helyesen kell beállítania. Cross-compilationnál ez külön figyelmet igényel: a host eszközöket és a target library-ket nem szabad összekeverni.

38.3.4 Statikus, shared és header-only integráció

A három integrációs forma nem csupán linkelési kényelem kérdése, hanem eltérő élettartam-, ABI- és telepítési következményekkel jár.

Forma Mit szállít Linkelés ABI-kitettség Tipikus következmény
Statikus .a vagy .lib a végső binárisba épül a kód a fogyasztó ABI-jával fordul tranzitív static függőségek a végső linkhez kellenek; PIC szükséges sharedbe építéshez
Shared .so vagy .dll, soname-mal futásidőben töltődik a library és a fogyasztó ABI-ja illeszkedjék DT_NEEDED, rpath, verziózott szimbólumok, futásidejű telepítés
Header-only fejlécek nincs külön linkelendő bináris inline definíciók és makrók viszik be az ABI-t ODR-fegyelem, fordítási idő, makrókonfiguráció-érzékenység

Statikus librarynél a fogyasztó linkparancsába bekerülnek a library saját, nem publikus linkigényei is, mert a statikus librarynek nincs saját linkelési lépése. A CMake ezt jellemzően $<LINK_ONLY:...> formában továbbítja. Shared librarynél a dinamikus loader oldja fel a függőségeket; itt a soname, az rpath és a szimbólumláthatóság válik kritikussá. Header-only esetben nincs linkelendő artefaktum, de ez nem jelenti, hogy nincs ABI-kapcsolat: az inline függvények, osztálydefiníciók és makrók a fogyasztó fordítási egységébe kerülnek, ezért eltérő makróbeállítás vagy eltérő standard library-verzió akár ODR-sértést vagy viselkedésbeli eltérést okozhat.

A tranzitív link CMake-ben a PRIVATE, PUBLIC és INTERFACE kulcsszavakkal szabályozható:

Kulcsszó Saját link Fogyasztó felé öröklődik
PRIVATE igen nem, statikus librarynál link-only formában mégis megjelenhet
PUBLIC igen igen
INTERFACE nem igen

A PUBLIC tehát nem „jobb” a PRIVATE-nál: a PUBLIC kiszélesíti a felületet, és a fogyasztót is a függőséghez köti. A helyes választás attól függ, hogy a függőség típusai megjelennek-e a saját nyilvános fejlécben. Ha igen, PUBLIC; ha csak a megvalósításban, PRIVATE.

Az ABI/compiler/stdlib kompatibilitás feltételei C++ esetben legalább a következők lehetnek: azonos C++ ABI-család, azonos standard library és annak ABI-konfigurációja (például libstdc++ vagy libc++), azonos -std= és fontosabb nyelvi kapcsolók, azonos kivétel- és RTTI-beállítások, azonos debug/release és iterator-debug szint, azonos sanitizer-instrumentáltság, valamint azonos struct packing/alignment opciók. A libstdc++ esetében például a _GLIBCXX_USE_CXX11_ABI eltérése külön bináris ABI-t eredményezhet. Ezek a feltételek nem teljes listák; a konkrét toolchain dokumentációja és a tényleges bináris vizsgálata dönt. C-oldali határon az extern "C" megszünteti a névtorzítást, de a hívási konvenció, a struct-layout és az alignment továbbra is szerződés része marad.

38.3.5 Beszerzés és rögzítés: FetchContent, vcpkg, Conan

A FetchContent forrásszinten hozza be a függőséget a buildbe. Előnye, hogy a függőség ugyanazzal a compilerrel és ugyanazokkal a kapcsolókkal fordul, mint a projekt; hátránya, hogy a konfigurációs lépés hálózatot és plusz fordítási időt igényelhet, valamint a függőség saját buildje bekerül a projekt buildgráfjába. Rögzítés nélkül nem reprodukálható:

include(FetchContent)

FetchContent_Declare(
  textutil
  SOURCE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/vendor/textutil
)

FetchContent_MakeAvailable(textutil)

Hálózati letöltésnél a GIT_TAG értéke lehetőleg commit hash legyen, ne mozgatható tag; URL-es letöltésnél URL_HASH SHA256=... szükséges. A konkrét forrás URL-jét és hash-ét a projekt saját, ellenőrzött dependency-jegyzékéből kell venni; ez a fejezet nem rögzít third-party verziót.

A vcpkg és a Conan magasabb szintű csomagkezelést ad: manifestet, verziófeloldást, bináris cache-t, triplet- vagy setting-alapú fordítási konfigurációt. A vcpkg manifest esetében a builtin-baseline és a triplet rögzíti a kiinduló állapotot; Conan esetében a lockfile, a recipe-revízió és a compiler/stdlib setting teszi reprodukálhatóvá a feloldást. Egyik eszköz sem menti meg a projektet attól, hogy a verziótartományt lock nélkül használja: a „legújabb kompatibilis” feloldás nem reprodukálható build. A konkrét manifest-séma és mezőnevek verziófüggők, ezért a használt kiadás dokumentációját kell ellenőrizni.

38.3.6 License és attribution megőrzése

A függőség forráscsomagjában maradjon meg a licenc és a hozzá tartozó attribution: LICENSE, COPYING, NOTICE, valamint a szerzői és származási információk. Hasznos egy ORIGIN.txt a függőség könyvtárában a forrás, a verzió, a hash és a beszerzés dátumával. SPDX-azonosító használható a gépi feldolgozáshoz, de a jogi tartalmat a licenc szövege adja. Statikus linkelésnél és shared librarynél eltérő kötelezettségek merülhetnek fel; ezeket nem a buildrendszer dönti el, hanem a licenc. A fejezet nem ad jogi tanácsot: a konkrét licencszöveget és a projekt jogi policy-ját kell követni.

38.3.7 Kidolgozott példa: saját package config és fogyasztó

Az alábbi példa egy saját textutil libraryt telepít CMake package configként, majd egy fogyasztó find_package-pel keresi, és ha nem találja, forrásból építi be. A kód illusztráció; a fordítás és futtatás tényét ez a szöveg nem állítja.

third_party/textutil/include/textutil/textutil.hpp:

#pragma once
#include <string>
#include <vector>

namespace textutil {
std::string join(const std::vector<std::string>& parts, const std::string& sep);
}

third_party/textutil/src/textutil.cpp:

#include "textutil/textutil.hpp"

namespace textutil {
std::string join(const std::vector<std::string>& parts, const std::string& sep) {
    std::string out;
    for (std::size_t i = 0; i < parts.size(); ++i) {
        if (i != 0) out += sep;
        out += parts[i];
    }
    return out;
}
}

third_party/textutil/CMakeLists.txt:

cmake_minimum_required(VERSION 3.24)
project(textutil VERSION 1.2.0 LANGUAGES CXX)

add_library(textutil src/textutil.cpp)
add_library(textutil::textutil ALIAS textutil)

target_include_directories(textutil
  PUBLIC
    $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
    $<INSTALL_INTERFACE:include>)
target_compile_features(textutil PUBLIC cxx_std_20)

include(GNUInstallDirs)
include(CMakePackageConfigHelpers)

install(TARGETS textutil EXPORT textutilTargets
  ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}
  LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
  RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR})
install(DIRECTORY include/ DESTINATION ${CMAKE_INSTALL_INCLUDEDIR})
install(EXPORT textutilTargets
  NAMESPACE textutil::
  DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/textutil)

configure_package_config_file(
  cmake/textutilConfig.cmake.in
  ${CMAKE_CURRENT_BINARY_DIR}/textutilConfig.cmake
  INSTALL_DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/textutil)
write_basic_package_version_file(
  ${CMAKE_CURRENT_BINARY_DIR}/textutilConfigVersion.cmake
  VERSION ${PROJECT_VERSION}
  COMPATIBILITY SameMajorVersion)
install(FILES
  ${CMAKE_CURRENT_BINARY_DIR}/textutilConfig.cmake
  ${CMAKE_CURRENT_BINARY_DIR}/textutilConfigVersion.cmake
  DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/textutil)

A cmake/textutilConfig.cmake.in minimális tartalma:

@PACKAGE_INIT@
include("${CMAKE_CURRENT_LIST_DIR}/textutilTargets.cmake")
check_required_components(textutil)

A fogyasztó CMakeLists.txt:

cmake_minimum_required(VERSION 3.24)
project(depdemo LANGUAGES CXX)

find_package(textutil 1.2 CONFIG QUIET)
if(NOT textutil_FOUND)
  add_subdirectory(third_party/textutil)
endif()

add_executable(depdemo app/main.cpp)
target_link_libraries(depdemo PRIVATE textutil::textutil)

app/main.cpp:

#include <textutil/textutil.hpp>
#include <iostream>

int main() {
    std::cout << textutil::join({"a", "b", "c"}, ",") << '\n';
}

Build- és telepítési parancsok:

cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release
cmake --build build
cmake --install build --prefix "$PWD/stage"
cmake -S . -B build-installed -G Ninja -DCMAKE_PREFIX_PATH="$PWD/stage"
cmake --build build-installed

A második konfigurálásnál a find_package a telepített textutilConfig.cmake-t találja meg; az elsőnél a QUIET miatt csendben a add_subdirectory ágra esik. A várt kimenet a,b,c, de ez illusztráció, nem mért eredmény. A SameMajorVersion választás tudatos: a fogyasztó 1.x-en belül frissíthet, 2.0-nál újra kell vizsgálni az ABI-t és a forráskompatibilitást.

38.3.8 Tipikus hibák és javításuk

Az egyik gyakori hiba, hogy a projekt csak include-könyvtárat ad hozzá, de nem linkel imported targetet. Ilyenkor a fejléc fordul, a linkelés viszont hiányzó szimbólumnál elhasal, vagy ami rosszabb, egy másik, ABI-inkompatibilis bináris kerül a linkparancsba. Javítás: find_package és target_link_libraries a csomag által kínált targettel.

A második hiba a verzió nélküli find_package. Ez csendben más verziót oldhat fel a fejlesztői gépen és a CI-ben. Javítás: verziómegkötés és a csomag verziókompatibilitási policy-jének tudatos megválasztása, valamint a feloldott verzió naplózása.

A harmadik hiba a PUBLIC indokolatlan használata. Ettől a fogyasztó is linkelni fogja a függőséget, nő a bináris felület és az ABI-kitettség. Javítás: a nyilvános fejlécben megjelenő típusoknál PUBLIC, a csak megvalósításban használt függőségnél PRIVATE.

A negyedik hiba a különböző standard library vagy compiler-beállítás összekeverése. Például az egyik objektum debug iteratorokkal, a másik anélkül fordul, vagy eltér a _GLIBCXX_USE_CXX11_ABI értéke. Javítás: egységes toolchain-profil, ABI-ellenőrzés a binárison, és a függőségek forrásból fordítása ott, ahol ez indokolt.

Az ötödik hiba a FetchContent rögzítés nélküli használata. Egy mozgó branch vagy tag mellett a build idővel más kódot fordf. Javítás: commit hash vagy URL_HASH, és a beszerzett forrás hash-ének rögzítése a forráscsomagban.

38.3.9 Összefoglaló

A függőség nem csak egy -l kapcsoló. A discovery módja, az imported target használati követelményei, a statikus/shared/header-only forma, a tranzitív link és az ABI-feltételek együtt adják a szerződést. A reprodukálhatóságot a verzió és a forrás rögzítése, valamint a lockolt feloldás adja; a jogi megfelelést a licenc- és attribution-adatok megőrzése. A CMake target-alapú modellje akkor működik jól, ha a függőségeket targetként, nem globális változóként kezeljük.

38.3.10 Feladatok

  1. Magyarázd el, miért jobb a target_link_libraries(app PRIVATE fmt::fmt) forma, mint az include_directories(...), link_directories(...) és target_link_libraries(app PRIVATE fmt) kombinációja. Adj legalább három, a build helyességéhez és a karbantarthatósághoz kapcsolódó érvet.

  2. Egy app executable egy A statikus libraryt linkel. Az A a saját CMakeLists.txt-jében PRIVATE módon használja B-t. A fogyasztó linkelése mégis „undefined reference” hibát ad B szimbólumaira. Magyarázd meg, miért történik ez, és add meg a helyes javítást. Miért nem jó megoldás, ha B-t egyszerűen PUBLIC-ra váltjuk?

  3. Tervezz dependency-policy-t egy projekthez, amely egy header-only libraryt, egy shared libraryt és egy statikus libraryt használ. A policy térjen ki a verzió-rögzítésre, a discovery módjára, az ABI-ellenőrzésre és a licencadatok megőrzésére. Adj hozzá CMake-snippetet is.

38.3.11 Megoldások

  1. Az imported target egy helyen tartalmazza az include-könyvtárat, a szükséges fordítási definíciókat, a nyelvi szintet és a tranzitív linkigényeket. A kézi változók ezt szétszórják, és a fogyasztó könnyen kihagy egy követelményt. Az imported target kezeli a konfigurációspecifikus útvonalakat és a telepített vagy forrásból épített változat közötti különbséget. Végül a target-alapú forma jobban ellenőrizhető: a linkparancs és a tulajdonságok egy objektumban vannak, nem globális állapotban. Ellenőrzési feltétel: a fogyasztó CMakeLists.txt-jében ne szerepeljen kézi include- és linkútvonal a függőséghez.

  2. A statikus librarynek nincs saját linkelési lépése, ezért a PRIVATE linkigénye nem tűnik el teljesen: a CMake a fogyasztó végső linkjéhez $<LINK_ONLY:B> formában továbbítja. Ha a fogyasztó mégis hiányzó szimbólumot lát, az vagy abból ered, hogy a B nem érhető el a linkparancsban, vagy abból, hogy a B nem ugyanazzal az ABI-val készült. A helyes javítás: a B targetjét elérhetővé tenni az A exportjában, illetve a fogyasztó linkparancsában; ha a B csak implementációs részlet, maradjon PRIVATE, de a statikus linkhez szükséges formában. A PUBLIC azért nem jó, mert a B típusai nem jelennek meg az A nyilvános fejlécében, így feleslegesen kiterjesztené a felületet és a fogyasztó ABI-kitettségét. Ellenőrzési feltétel: a fogyasztó linkelése sikeres, és az A nyilvános fejlécében nem jelenik meg B-típus.

  3. Példapolicy: a header-only függőséget rögzített commit hash-sel vagy verziózott forráscsomaggal kell behozni, és dokumentálni kell a szükséges makrókat. A shared libraryt find_package(... CONFIG)-pal kell keresni, verziómegkötéssel és SameMajorVersion policy-val; a telepítésnél ellenőrizni kell a soname-et és az rpath-t. A statikus libraryt szintén targetként kell linkelni, és a tranzitív igényeit a CMake exportjában kell átadni. Az ABI-ellenőrzéshez a projekt rögzítse a compilert, a standard libraryt, a -std= értéket és a fontos ABI-kapcsolókat. A licencadatok a vendor/<name>/ könyvtárban maradjanak, LICENSE, NOTICE és ORIGIN.txt formában. CMake-részlet:

find_package(sharedlib 2 CONFIG REQUIRED)
add_subdirectory(vendor/headeronly)
add_subdirectory(vendor/staticlib)

add_executable(app src/main.cpp)
target_link_libraries(app PRIVATE
  sharedlib::sharedlib
  headeronly::headeronly
  staticlib::staticlib)

Ellenőrzési feltétel: a build három különböző integrációs formát használ, a verziók rögzítettek, és a forráscsomag tartalmazza a licenc- és származási adatokat. A példa nem állítja, hogy a konkrét függőségek elérhetők vagy lefordultak; a verziókat és az ABI-kapcsolókat a választott toolchain dokumentációja alapján kell ellenőrizni.