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.
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.
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.
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.
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.
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.
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.
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-installedA 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.
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.
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.
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.
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?
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.
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.
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.
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.