Egy natív release nem „a bináris”, hanem egy artefaktumcsomag: a futtatható fájl, a hozzá tartozó hibakeresési adat, a build azonosítója, a függőségi leírás és a telepítési szerződés együtt. Ez a rész ezt a csomagot építi fel, abból a DevOps-szempontból, hogy üzem közben egy összeomlás után vissza kell tudni keresni a pontos forrásállapotot, a hibakeresési információt pedig nem szabad a termelési image-be tenni. A módszerek és a parancsok itt a tanuló laborjában futtathatók; a könyv nem állítja, hogy ezeket a példákat lefuttatta, és a konkrét kapcsolónevek, alapértelmezések a használt GCC/Clang, binutils és CMake kiadásától függenek — ezt minden lépésnél ellenőrizni kell a rögzített laborprofilhoz tartozó dokumentációban.
A release-szerződést négy, egymástól függetlenül ellenőrizhető
tulajdonságra bontjuk. Reprodukálhatóság (reproducible
build): ugyanaz a forrás, ugyanaz a toolchain-profil, ugyanazok
a kapcsolók és ugyanaz a normalizált környezet bitre azonos kimenetet
ad. Feltétele, hogy a buildben ne maradjon időbélyeg, abszolút útvonal,
véletlen seed vagy nem determinisztikus sorrend.
Visszakereshetőség (traceability): a leszállított
fájlból egyértelműen eljutunk a forrásállapothoz, a fordítóhoz és a
függőségekhez; ezt a build-id és a hozzá kapcsolt metaadat
biztosítja. Telepíthetőség (deployability): a csomag
tetszőleges prefixbe kicsomagolható, és a dinamikus loader a saját
könyvtáraiból oldja fel a függőségeket. Hibakereshetőség
(debuggability): a termelési binárison nincs szimbólum, de a
hozzá tartozó külön debug fájl alapján a core dump teljes backtrace-re
és forrássorra bontható.
A négy tulajdonság közül bármelyik kimarad, a hiány csak üzemzavar után derül ki: a visszakereshetetlen buildnél nem tudjuk, melyik commitot kell javítani; a szimbolizálhatatlan core dumpnál csak címeket látunk; a nem relocatable csomagnál a konténer másik útvonalon nem indul el.
A fejezet hátralevő részében egy kéttagú projektet használunk: a
libgreet megosztott könyvtárat és a greetctl
CLI-t. A kód szándékosan kicsi, hogy a build-szerződés legyen a nehéz
rész.
// include/greet/greet.hpp
#pragma once
#include <string>
#include <string_view>
namespace greet {
std::string greeting(std::string_view name);
const char* version() noexcept;
}// src/greet.cpp
#include "greet/greet.hpp"
namespace greet {
std::string greeting(std::string_view name) {
return "hello, " + std::string{name};
}
const char* version() noexcept { return GREET_VERSION; }
}// src/main.cpp
#include "greet/greet.hpp"
#include <cstdio>
#include <string_view>
int main(int argc, char** argv) {
if (argc == 2 && std::string_view{argv[1]} == "--version") {
std::printf("greetctl %s\n", greet::version());
return 0;
}
const std::string out = greet::greeting(argc > 1 ? argv[1] : "world");
std::printf("%s\n", out.c_str());
return 0;
}A target-alapú CMake telepítési modellje három külön dologból áll: a
binárisok fizikai másolása (install(TARGETS ...)), az
IMPORTED targeteket leíró export fájl
(install(EXPORT ...)) és a csomag belépési pontja
(greetConfig.cmake). A relocatable config feltétele, hogy a
config fájl ne tartalmazzon abszolút build útvonalat, hanem a saját
helyéhez képest számolja a prefixet. Ezt a
configure_package_config_file generálja: a
@PACKAGE_INIT@ blokk definiálja a
PACKAGE_PREFIX_DIR változót a config fájl tényleges
helyéből visszafelé számolva, így a csomag bármely prefixbe
áthelyezhető.
cmake_minimum_required(VERSION 3.25)
project(greet VERSION 1.4.2 LANGUAGES CXX)
add_library(greet SHARED src/greet.cpp)
add_library(greet::greet ALIAS greet)
target_compile_features(greet PUBLIC cxx_std_20)
target_compile_definitions(greet PRIVATE GREET_VERSION="${PROJECT_VERSION}")
target_include_directories(greet PUBLIC
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
$<INSTALL_INTERFACE:include>)
set_target_properties(greet PROPERTIES
VERSION ${PROJECT_VERSION}
SOVERSION 1)
add_executable(greetctl src/main.cpp)
target_link_libraries(greetctl PRIVATE greet::greet)
set_target_properties(greetctl PROPERTIES
INSTALL_RPATH "$ORIGIN/../lib")
include(GNUInstallDirs)
install(TARGETS greet greetctl
EXPORT greetTargets
LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR})
install(DIRECTORY include/ DESTINATION ${CMAKE_INSTALL_INCLUDEDIR})
install(EXPORT greetTargets
NAMESPACE greet::
DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/greet)
include(CMakePackageConfigHelpers)
configure_package_config_file(cmake/greetConfig.cmake.in
${CMAKE_CURRENT_BINARY_DIR}/greetConfig.cmake
INSTALL_DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/greet)
write_basic_package_version_file(
${CMAKE_CURRENT_BINARY_DIR}/greetConfigVersion.cmake
COMPATIBILITY SameMajorVersion)
install(FILES
${CMAKE_CURRENT_BINARY_DIR}/greetConfig.cmake
${CMAKE_CURRENT_BINARY_DIR}/greetConfigVersion.cmake
DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/greet)A cmake/greetConfig.cmake.in tartalma a
@PACKAGE_INIT@ után mindössze két hívás:
@PACKAGE_INIT@
include("${CMAKE_CURRENT_LIST_DIR}/greetTargets.cmake")
check_required_components(greet)A CMAKE_CURRENT_LIST_DIR használata az, ami
prefixfüggetlenné teszi a fájlt. Gyakori hiba, hogy valaki ilyen sort
generál bele:
set(GREET_INCLUDE_DIR /home/fejleszto/greet/include) # HIBA: abszolút útvonalEz a csomagot pontosan egy géphez köti.
A SONAME a megosztott könyvtár dinamikus
szekciójában tárolt név (DT_SONAME), amelyet a loader a
függőségek feloldásához használ. Amikor a greetctl-t a
libgreet.so-hoz linkeljük, a linker a
DT_NEEDED bejegyzésbe a SONAME-et írja, ha van; a
SOVERSION 1 beállítás tipikus hatása a
libgreet.so.1 SONAME, a VERSION pedig a
libgreet.so.1.4.2 fájlnevet és a symlinkeket állítja elő. A
verziózás lényege, hogy egy ABI-kompatibilis javítás ugyanazt a
SONAME-et kapja, egy inkompatibilis változás új SOVERSION-t.
A rpath (DT_RPATH) és a
runpath (DT_RUNPATH) a binárisba írt
könyvtárkeresési bejegyzés. Különbségük sorrendben és hatókörben van: a
DT_RPATH-et a loader a LD_LIBRARY_PATH előtt,
a DT_RUNPATH-et utána nézi, és a DT_RUNPATH
nem tranzitív — csak annak az objektumnak a közvetlen
függőségeire érvényes, amelyben szerepel. Ez a legtöbb „működött a
gépen, elszállt a konténerben” hiba gyökere. A $ORIGIN
tokent a loader annak az objektumnak a könyvtárára cseréli, amelyben a
bejegyzés található, ezért a telepített CLI-nél a
$ORIGIN/../lib alak a helyes, nem a build könyvtár abszolút
útja. Hogy a linker DT_RPATH vagy DT_RUNPATH
bejegyzést készít-e, az a --enable-new-dtags /
--disable-new-dtags kapcsolótól és a binutils verziójától
függ; ezt a laborprofilban rögzíteni kell.
Ellenőrzésre ne az ldd-t használjuk ismeretlen
binárison: a glibc ldd implementációja a dynamic loader
trace-módjában lefuttatja a programot, ami nem kívánt kódvégrehajtást
jelenthet. Helyette readelf -d vagy objdump -p
adja a NEEDED, RPATH/RUNPATH és
SONAME bejegyzéseket.
A build-id a linker által a kimenet tartalmából
számolt azonosító a .note.gnu.build-id szekcióban. Erre
épül a symbolication: a GDB a core dumpból kiolvasott build-id alapján
keresi meg a hozzá tartozó debug fájlt a debug-könyvtárban vagy egy
debuginfod szolgáltatásban. A build-id jelenléte nem automatikus minden
platformon, ezért explicit kérés érdemes:
-Wl,--build-id=sha1.
A külön debug szimbólum előállítása három lépés: a
teljes hibakeresési adat kiemelése, a bináris megfosztása a debug
szekcióktól, majd egy .gnu_debuglink szekció beírása, amely
a debug fájl nevét és egy CRC32 ellenőrzőértéket tartalmaz. A debug fájl
a build-id note-ot is megtartja, így két úton is megtalálható.
f=stage/opt/greet/bin/greetctl
objcopy --only-keep-debug "$f" "$f.debug"
objcopy --strip-debug --add-gnu-debuglink="$f.debug" "$f"A --strip-debug csak a .debug_* szekciókat
távolítja el, a .symtab-ot nem; a strip ennél
agresszívabb, és a lokális szimbólumneveket is elveszi, ami a backtrace
minőségét rontja. Optimalizált buildben a -g megmarad, de a
változók egy része <optimized out> lesz, és az
inlineolt hívások több forrássort rendelnek egy frame-hez — ez nem hiba,
hanem az optimalizálás következménye. A -gsplit-dwarf külön
.dwo fájlokat eredményez, ami a tárolást csökkenti, de a
disztribúciót bonyolítja; csak tudatos döntéssel használjuk.
A reprodukálhatóság azt jelenti, hogy a build
bemenetei és a normalizált környezet rögzítése mellett a kimenet bitre
azonos. A SOURCE_DATE_EPOCH egy szabványos környezeti
változó, amelyet sok eszköz (például ar, tar,
egyes fordítók a __DATE__/__TIME__ makróhoz)
az időbélyegek helyett használ. A forrása tipikusan a commit
időpontja:
export SOURCE_DATE_EPOCH=$(git log -1 --format=%ct)A path mapping a debug info-ba és a
__FILE__ makróba kerülő abszolút útvonalakat írja át. A
-ffile-prefix-map=OLD=NEW kapcsoló a debug- és
makróútvonalakat egyszerre kezeli; a GCC és a Clang elérhetősége és
pontos neve kiadásfüggő (a Clangnál külön
-fdebug-prefix-map és -fmacro-prefix-map is
létezik). A tipikus beállítás:
-ffile-prefix-map=$PWD=.Nondeterminizmus forrásai és kezelésük:
| Forrás | Tünet | Kezelés |
|---|---|---|
| Abszolút build út a debug infóban | két könyvtárból építve eltérő hash | -ffile-prefix-map |
__DATE__, __TIME__ |
minden build más | SOURCE_DATE_EPOCH, vagy a makrók elhagyása |
ar archívum időbélyeg |
.a fájl eltér |
determinisztikus mód, ellenőrizni kell |
| Fordító által generált belső nevek | anonim névtér/lambda szimbólumok eltérnek | -frandom-seed=<rögzített> (GCC) |
| LTO párhuzamosság | partition-sorrend eltér | rögzített -flto=1 vagy determinisztikus beállítás |
| Nem rendezett fájllista | link-sorrend eltér | explicit, rögzített forráslista |
Az ellenőrzés két, különböző könyvtárból indított build
hash-összehasonlítása; eltérés esetén a diffoscope jellegű
eszközök mutatják meg a konkrét eltérő szekciót. Fontos: a build-id a
tartalom hash-e, tehát ha a tartalom azonos, a build-id is azonos — a
build-id önmagában nem nondeterminizmus-forrás.
Az SBOM (software bill of materials) géppel olvasható komponenslista: az artefaktumban ténylegesen felhasznált könyvtárak neve, verziója, beszállítója, hash-e és a köztük lévő függőségi élek. Formátuma lehet SPDX vagy CycloneDX; a kötelező mezők halmaza a formátum verziójától függ, ezért a generáló eszközt és annak verzióját a buildprofilban rögzíteni kell. Az SBOM-ot a build-id-hez kell kötni, hogy a leszállított fájl és a sor közötti kapcsolat ellenőrizhető legyen. Az SBOM nem bizonyítja a sebezhetetlenséget, és nem helyettesíti a függőségek tényleges verzióinak ellenőrzését — ezt a témakör határait tiszteletben tartva itt nem tárgyaljuk.
A libc baseline az a legrégebbi glibc-verzió,
amelyen a bináris hajlandó elindulni. Nem a fordító verziója, hanem a
felhasznált szimbólumverziók halmaza határozza meg: a dinamikus loader a
legmagasabb GLIBC_x.y igényt ellenőrzi, és ha a célrendszer
régebbi, version 'GLIBC_2.34' not found hibával áll le.
Ugyanez vonatkozik a libstdc++ GLIBCXX_ és
CXXABI_ szimbólumaira, valamint a
_GLIBCXX_USE_CXX11_ABI beállításra, amely a
std::string ABI-ját befolyásolja. A minimumot így olvassuk
ki:
readelf --version-info stage/opt/greet/bin/greetctl \
| grep -o 'GLIBC_[0-9.]*' | sort -Vu | tail -1A minimális runtime image csak a szükséges
SONAME-eket tartalmazza; a listát a readelf -d
NEEDED bejegyzései adják, nem az ldd kimenete.
A teljesen statikus linkelés bizonyos libc-funkciók (névfeloldás,
dlopen) esetén külön figyelmeztetéseket hordoz, ezért nem
alapértelmezett választás.
A smoke test a telepítési kapu: a cél-image-ben lefuttatott minimális parancs, amelynek nem nulla kilépési kódja megbuktatja a deployt. Feltétele, hogy a teszt a valódi image-en fusson, ne a build gépen.
#!/usr/bin/env bash
set -euo pipefail
img="$1"
docker run --rm "$img" /usr/local/bin/greetctl --version
docker run --rm "$img" /usr/local/bin/greetctl AdaA példaprojekt buildje a fenti szerződés mind a négy elemét lefedi. A build és a telepítés staged módon történik, hogy a prefix később szabadon mozgatható legyen:
export SOURCE_DATE_EPOCH=$(git log -1 --format=%ct)
cmake -S . -B build -G Ninja \
-DCMAKE_BUILD_TYPE=RelWithDebInfo \
-DCMAKE_CXX_FLAGS="-ffile-prefix-map=$PWD=." \
-DCMAKE_INSTALL_PREFIX=/opt/greet
cmake --build build
DESTDIR="$PWD/stage" cmake --install buildA RelWithDebInfo konfiguráció jellemzően
-O2 -g kapcsolókat ad, de ez a CMake verziójától függ,
ezért a laborprofilban ellenőrizni kell. A telepítés után a binárist
debug szétválasztással látjuk el, majd ellenőrizzük a dinamikus
bejegyzéseket:
f=stage/opt/greet/bin/greetctl
objcopy --only-keep-debug "$f" "$f.debug"
objcopy --strip-debug --add-gnu-debuglink="$f.debug" "$f"
readelf -d "$f" | grep -E 'NEEDED|RPATH|RUNPATH'
readelf -n "$f" | grep 'Build ID'A várt értelmezés: a NEEDED között
libgreet.so.1 és a C++ runtime könyvtárai szerepelnek, a
RUNPATH pedig $ORIGIN/../lib alakú, nem
abszolút. A debug szétválasztás után a greetctl mérete
csökken, a greetctl.debug megtartja a build-id note-ot, és
a core dump szimbolizálásához a debug fájlt a bináris mellé vagy a
debug-könyvtárba kell tenni.
A leggyakoribb, hogy a release pipeline strip-pel
„tisztítja” a binárist, és a debug fájlt nem őrzi meg — így a későbbi
core dump csak címeket mutat. A második, hogy a
CMAKE_INSTALL_PREFIX abszolút értéke bekerül a config
fájlba, és a csomag áthelyezés után nem találja magát. A harmadik, hogy
a RUNPATH-tól tranzitív feloldást várunk: a
libgreet.so.1 saját függőségeit a saját
RUNPATH-ja vagy a rendszerkönyvtárak alapján keresi a
loader. A negyedik, hogy a SOURCE_DATE_EPOCH-ot beállítjuk,
de a kódban marad __TIME__, vagy a verziósztringbe build
idő kerül. Az ötödik, hogy ldd-vel ellenőrzünk egy nem
megbízható binárist.
A release-szerződés négy, külön ellenőrizhető tulajdonságból áll:
reprodukálhatóság, visszakereshetőség, telepíthetőség és
hibakereshetőség. A CMake export és a
configure_package_config_file a prefixfüggetlenséget, a
SONAME és a $ORIGIN-alapú RUNPATH a dinamikus feloldást, a
build-id és a .gnu_debuglink a symbolicationt, a
SOURCE_DATE_EPOCH és a -ffile-prefix-map a
determinizmust biztosítja. A deploy kapuja a cél-image-en futtatott
smoke test és a libc-baseline ellenőrzése.
1. feladat (magyarázó). Egy kolléga a
libgreet.so-t ezzel a kapcsolóval linkeli:
-Wl,-rpath,/home/fejleszto/greet/buildA konténerben a CLI nem indul. Magyarázd meg, miért nem működik, és
miért nem orvosolja a problémát az, ha a RUNPATH-ot
$ORIGIN-ra cseréljük a greetctl-n, miközben a
libgreet.so saját függőségei a build könyvtárra
mutatnak.
2. feladat (kódolási). Egészítsd ki a
CMakeLists.txt-t úgy, hogy a telepített
greetctl $ORIGIN/../lib RUNPATH-ot kapjon, a
libgreet SONAME-je libgreet.so.1 legyen, és a
debug szétválasztás egy külön telepítési komponensbe kerüljön. Add meg a
hozzá tartozó objcopy parancsokat és az ellenőrző
readelf hívásokat.
3. feladat (diagnosztika). A readelf -d
kivonatban a greetctl NEEDED listája
libgreet.so.1, libstdc++.so.6,
libc.so.6, RUNPATH értéke
$ORIGIN/../lib. A libgreet.so.1
NEEDED listája libm.so.6,
libc.so.6, RUNPATH nélkül. Melyik függőség
oldódik fel, és melyik nem, ha a libm.so.6 csak a csomag
lib/ alkönyvtárában van, a rendszerkönyvtárban nem?
4. feladat (ellenőrzés). Tervezz olyan ellenőrző lépéssort, amely két különböző könyvtárból indított build kimenetéről eldönti, hogy reprodukálható-e, és fogalmazd meg, mit teszel eltérés esetén.
1. megoldás. Az abszolút rpath a build gép
könyvtárára mutat, amely a konténerben nem létezik, ezért a loader nem
találja a libgreet.so.1-et, és
cannot open shared object file hibával leáll. A
$ORIGIN-ra cserélés a greetctl-n csak a
greetctl közvetlen függőségeit oldja fel, mert a
DT_RUNPATH nem tranzitív: a libgreet.so saját
függőségeit a saját bejegyzései alapján keresi a loader. Ezért a
libgreet.so-t is $ORIGIN-alapú RUNPATH-tal
kell linkelni (vagy a függőségeit a rendszerkönyvtárba telepíteni).
Ellenőrzési feltétel: a readelf -d mindkét objektumban
$ORIGIN-t tartalmazó RUNPATH-ot mutat, és a
lib/ könyvtárban minden NEEDED fájl
megtalálható.
2. megoldás. A CMakeLists.txt-be a
set_target_properties(greetctl PROPERTIES INSTALL_RPATH "$ORIGIN/../lib")
és a
set_target_properties(greet PROPERTIES SOVERSION 1 VERSION ${PROJECT_VERSION})
sorok kerülnek. A debug szétválasztáshoz a telepítés után futtatjuk az
objcopy --only-keep-debug,
objcopy --strip-debug --add-gnu-debuglink hármast, majd a
.debug fájlt egy külön komponensbe telepítjük. Ellenőrzés:
readelf -d a SONAME-ben
libgreet.so.1-et, readelf -n build-id note-ot
mutat, a .debug fájl pedig a greetctl mellett
vagy a debug-könyvtárban elérhető.
3. megoldás. A greetctl közvetlen
függőségei (libgreet.so.1, libstdc++.so.6,
libc.so.6) a RUNPATH és a rendszerkönyvtárak
alapján feloldódnak, ha a libgreet.so.1 a
../lib-ben van. A libm.so.6 viszont a
libgreet.so.1 függősége, és mivel annak nincs
RUNPATH-ja, a loader csak a rendszerkönyvtárakban keresi —
ott pedig nincs, ezért a betöltés meghiúsul. Javítás: a
libm.so.6-ot a rendszerkönyvtárba tenni, vagy a
libgreet.so.1-et $ORIGIN-alapú RUNPATH-tal
újralinkelni.
4. megoldás. Két külön build könyvtárból
(build-a, build-b), azonos
SOURCE_DATE_EPOCH, azonos prefix-map és azonos
toolchain-profil mellett lefordítjuk a projektet, majd
sha256sum-mal összehasonlítjuk a leszállított binárist, a
könyvtárat és a debug fájlt. Eltérés esetén először a
readelf -p .comment és a debug szekciók útvonalai alapján
keresünk be nem normalizált abszolút utat, majd a
__DATE__/__TIME__ előfordulásokat, végül az
archívumok és LTO partitionök sorrendjét vizsgáljuk. Az ellenőrzés akkor
tekinthető sikeresnek, ha a hash-ek bitre egyeznek, és a build-id is
azonos.