Następca OSTree: content-addressable, zorientowany blokowo, bez hardlinków.
Biblioteka + CLI w Nim (główny target), z bezpiecznym wrapperem Rust
(fasttree-rs) nad stabilnym C ABI (src/fasttree/capi.nim).
| Cecha | OSTree | FastTree |
|---|---|---|
| Struktura repo | zbiór plików + hardlinki (/ostree/repo) |
jeden plik obrazu (Composefs/EROFS) + CAS na chunkach |
| Haszowanie | SHA-256, jednowątkowe | BLAKE3, drzewo Merkle, wielowątkowe |
| I/O i aktualizacje | wywołania POSIX per plik | wsadowy io_uring (odczyt i zapis) |
| Weryfikacja integralności | przy przełączaniu commitów | dm-verity (device-mapper) + digest composefs |
| Modyfikacje lokalne | stateroot, nakładanie dystrybucyjne | natywne warstwy OverlayFS (ulotne/trwałe) |
| Deduplikacja | na poziomie całych plików (hardlink) | na poziomie chunków (FastCDC, zmienny rozmiar) |
| Garbage collection | ostree prune (po commitach) |
fasttree gc (po chunkach, live-set z CURRENT_TAG + pins) |
| Autoryzacja rejestru | n/d | pełny OCI Bearer token flow (401→token→retry) |
fasttree.nimble pakiet Nim — GŁÓWNY target projektu
Cargo.toml workspace Rust (obok fasttree.nimble) — wrapper nad C ABI
include/fasttree.h stabilny, ręcznie pisany nagłówek C ABI (źródło prawdy)
.github/workflows/ci.yml CI: nimble test, cargo test, testy integracyjne composefs/io_uring
src/fasttree/
hashing.nim BLAKE3 (FFI do libblake3, z runtime ABI sanity-check) z
fallbackiem SHA-256 (nimcrypto, -d:fasttreeNoBlake3)
chunker.nim FastCDC content-defined chunking (gear hash, min/avg/max)
store.nim lokalny CAS: put/get/has/missing/listAll/deleteObject/objectPath,
shardowany jak .git/objects, wersjonowany (plik FORMAT)
manifest.nim manifest = drzewo Merkle całego rootfs; buildManifest(),
diff(), serializacja JSON z formatVersion/hashAlgo/chunker
layers.nim rozpakowanie i scalanie warstw OCI (tar + whiteout/.wh.*
+ opaque whiteout .wh..wh..opq + hardlinki + xattrs/ownership)
gc.nim garbage collector store'a: live-set z CURRENT_TAG + pins.json
ioengine.nim I/O: domyślnie asyncdispatch, `-d:fasttreeIoUring` (Linux +
liburing-dev) — prawdziwy backend na io_uring (readBatch + writeBatch)
oci.nim klient OCI Distribution v2: manifest+blob GET, pełny Bearer
auth flow (401→token→retry), cache blobów, świeży klient per-request
composefs.nim budowa obrazu (przez wsadowe I/O ioengine) + mount.composefs +
dm-verity (veritysetup: format/verify/open/close)
overlay.nim warstwy OverlayFS (ulotne w tmpfs / trwałe na dysku) nad
zamontowanym obrazem composefs
cli.nim komendy: pull / status / deploy / pin / gc / overlay create|remove
capi.nim stabilne C ABI (uchwyty, kody błędów FtStatus, ft_pull_run,
ft_deploy_build_image) — granica dla Rust/C, patrz include/fasttree.h
src/fasttree.nim publiczny re-export API biblioteki Nim
src/fasttreecli.nim binarka `fasttree`
tests/ nimble test — chunker, hashing, store, manifest, layers, gc
fasttree-sys/ Rust: surowe bindingi extern "C" (1:1 z fasttree.h)
fasttree-rs/ Rust: bezpieczny wrapper (Store/Manifest z pull()/deploy_image(), Result, RAII)
oci.resolveImageLayerspobiera manifest OCI (z pełną obsługą Bearer auth: 401 →WWW-Authenticate→ token → retry) i wszystkie warstwy w kolejności dół→góra, cache'ując bloby wlayers-cache/.layers.materializeLayersrozpakowuje (tar --xattrs) i scala warstwy — whiteout (.wh.nazwa) usuwa plik z niższych warstw, opaque whiteout (.wh..wh..opq) czyści cały odziedziczony katalog, hardlinki i xattrs/ownership są zachowywane.manifest.buildManifestdzieli scalone drzewo na chunki FastCDC i zapisuje je dostore(dedup automatyczny). Manifest zapisywany jakomanifests/<tag>.jsonzformatVersion/hashAlgo/chunkerzamrożonymi.composefs.buildImageczyta chunki WSADOWO przezioengine.readBatch(jedenio_uring_submitna wszystkie unikalne chunki manifestu), pisze materializowane pliki WSADOWO przezwriteBatch, wołamkcomposefs --print-digesti liczy hash-tree dm-verity (dmVerityFormat).cli.cmdDeploypodmienia symlinkcurrentprzezrename(2)(atomowy A/B swap) i zapisujeCURRENT_TAG— potrzebne przez GC.fasttree gcliczy live-set chunków (referencje zCURRENT_TAG+pins.json) i usuwa z CAS wszystko poza tym zbiorem.- Opcjonalnie:
fasttree overlay create <nazwa> [--ephemeral]montuje bieżący obraz jako lowerdir i dokłada zapisywalną warstwę OverlayFS — trwałą (przeżywa restart, jak/etcw OSTree) albo ulotną (tmpfs, znika przy odmontowaniu).
Ten sam pipeline (kroki 1–4) jest też dostępny bezpośrednio z Rust, bez przechodzenia przez binarkę CLI:
let manifest = Manifest::pull("ghcr.io/org/repo:tag", cache_dir, work_dir, &store)?;
let deploy = manifest.deploy_image(&store, materialized_dir, output_image)?;
// deploy.image_digest, deploy.verity_root_hash gotowe do zapisania obok obrazuManifest (manifests/<tag>.json): formatVersion (int, rośnie WYŁĄCZNIE
przy zmianie niekompatybilnej wstecz), hashAlgo ("blake3" |
"sha256-fallback", informacyjne), chunker (min/avg/maxSize użyte przy
budowie), root (hex drzewa Merkle), entries[] (posortowane po path).
Czytelnik z formatVersion nowszym niż obsługiwany musi odrzucić plik.
Store (store/): objects/<hex[0:2]>/<hex[2:]> — jeden plik = jeden
obiekt, surowe bajty chunku. Plik FORMAT w korzeniu store'a zawiera wersję
layoutu (dziś 1).
Pełna specyfikacja ABI dla konsumentów spoza Nim: include/fasttree.h
(zero wyjątków przez granicę, kody błędów FtStatus, uchwyty opaque,
zarządzanie pamięcią przez ft_string_free/ft_bytes_free).
Zweryfikowane end-to-end (skompilowane i uruchomione, nie tylko nim check):
- ✅
chunker.nim— FastCDC; złapany i naprawiony realny bug: hash resetowany przy każdym cięciu psuł właściwość content-defined - ✅
hashing.nim— prawdziwy BLAKE3 przez FFI, zweryfikowany przeciw oficjalnym wektorom referencyjnym; runtime ABI sanity-check - ✅
store.nim— CAS z deduplikacją, wersjonowanie formatu,listAll/deleteObject/objectPathpod GC i wsadowe I/O - ✅
manifest.nim—buildManifest,diff, serializacja JSON z walidacją - ✅
layers.nim— whiteout/opaque whiteout, hardlinki (śledzenie device+inode, odtwarzane w destDir), xattrs i ownership (cp --preserve), wszystko przetestowane realnymtarzsetfattr/chown - ✅
gc.nim— live-set zCURRENT_TAG+pins.json, przetestowany - ✅
ioengine.nim— backendio_uring:writeBatchireadBatch, prawdziwe FFI doliburing, przetestowane realnym I/O (w tym podział partii większej niż głębokość kolejki); podłączone docomposefs.buildImage - ✅
oci.nim— pełny Bearer auth flow (401→token→retry), przetestowany przez fikcyjny rejestr HTTP z prawdziwym wyzwaniem 401; model "świeży klient per request" (znaleziony i naprawiony bug: reużywanie połączeniastd/httpclientmiędzy osobnymirequest()jest kruche wobec wielu realnych serwerów, w tym domyślnegopython -m http.server) - ✅
composefs.nim— zbudowany od zera cały pakiet composefs z GitHuba (apt go nie ma),requireToolwykrywa fałszywe binarki po bannerze (narzędzia composefs nie mają--version),buildImagezwraca prawdziwy digest, dm-verity (veritysetup:format/verify/open/close) przetestowane łącznie z wykryciem realnej manipulacji bajtem w obrazie - ✅
overlay.nim— OverlayFS ulotny (tmpfs) i trwały, przetestowane realnym montowaniem: trwały przeżywa odmontowanie, ulotny znika z tmpfs - ✅
cli.cmdPull/cmdDeploy/cmdGc/cmdOverlay*— pełny pipeline, przetestowany integracyjnie przez prawdziwą binarkęfasttree: pull z fikcyjnego rejestru (2 warstwy, whiteout) → deploy (obraz composefs + dm-verity) → status → pin → gc, wszystko zweryfikowane - ✅
capi.nim+include/fasttree.h— C ABI, w tymft_pull_run/ft_deploy_build_image(pełny cykl, nie tylko Store/Manifest); przetestowane programem C (put/get/manifest/JSON round-trip) i z Rust; naprawiony bug bezpieczeństwa:hashFromHexrzucało niekatalogowalnyDefectzamiastValueError— złe wejście z FFI zwaliłoby cały proces wołającego - ✅
fasttree-sys+fasttree-rs(Rust) — cały łańcuch Nim→C→Rust,Manifest::pull()+Manifest::deploy_image()przetestowane end-to-end z Rust przeciwko fikcyjnemu rejestrowi, root-hash identyczny jak przez CLI Nim na tym samym repo (potwierdzona spójność implementacji) - ✅ cross-compilation (
fasttree-sys/build.rs) — mapowanieCARGO_CFG_TARGET_ARCH/OSna--cpu/--osNim + auto-wykrywanie cross-gcc; zweryfikowane ręcznie dla Linux x86_64 → Linux aarch64 (wynikowy.opotwierdzony jakoELF 64-bit LSB relocatable, ARM aarch64) — pełnecargo build --target=...niemożliwe do przetestowania w tym środowisku (brakrust-stddla innych architektur, offline) - ✅
tests/— 34 testy jednostkowe,nimble test ⚠️ .github/workflows/ci.yml— napisany wg tego, co ręcznie zweryfikowano lokalnie, ale nie uruchomiony na prawdziwym GitHub Actions (brak dostępu do CI z tego środowiska) — składnia YAML zwalidowana, logika kroków odzwierciedla dokładnie polecenia użyte podczas developmentu
Zostało (świadomie poza zakresem tej iteracji):
dmVerityOpen(pełne device-mapper) —format/verify(czysto plikowe) przetestowane w 100%. DodanodeviceMapperAvailable()(preflight-check/dev/mapper/control) i test jednostkowy (tests/test_composefs_capability.nim) weryfikujący fast-fail z czytelnym błędem, gdy device-mapper niedostępny — potwierdzone empirycznie w tym środowisku (veritysetup openfaktycznie kończy sięCannot initialize device-mapper. Is dm_mod kernel module loaded?, dokładnie jak przewidywał wcześniejszy komentarz w kodzie)..github/workflows/ci.yml(full-integration) ma teraz dedykowany krok (modprobe dm_mod+ realnydmVerityOpen/dmVerityClose), który na GitHub-hostedubuntu-latest(pełna maszyna wirtualna, nie zagnieżdżony kontener) ma szansę faktycznie przejść — pozostaje do potwierdzenia na prawdziwym runnerze.mountVerified(composefs-o digest=) wymaga fs-verity hosta (osobne od dm-verity) — środowisko testowe go nie miało. DodanofsVerityKernelSupport()(preflight-checkCONFIG_FS_VERITYw/boot/config-$(uname -r)) z tym samym testem jednostkowym..github/workflows/ci.ymlma teraz krok, który buduje ext4 z-O verityw pliku pętli i próbujefsverity enable+mountVerifiednaprawdę — w tym środowisku krok przeszedł aż do próbyfsverity enable, która zwróciłaOperation not supported(jądro sandboksa nie maCONFIG_FS_VERITY), więc kod poprawnie i bezpiecznie pominął resztę zamiast fałszywie zaliczyć test.dmVerityFormat/Verify/Opento niezależna, przetestowana ścieżka integralności.- Oba powyższe kroki CI, plus istniejący test composefs+dm-verity
format/verify, zostały faktycznie uruchomione lokalnie w tym środowisku
(zbudowano
composefsz źródeł, zainstalowanocryptsetup-bin/fsverity) — przy okazji znaleziono i naprawiono dwa niezależne bugi wci.ymlniewykryte wcześniej (patrz "Poprawki w tej iteracji" niżej). Samfull-integrationjob w tej postaci nadal nie był uruchomiony na prawdziwym GitHub Actions.
nimble install nimcrypto
nimble test # 34 testy, fallback SHA-256
FASTTREE_TEST_BLAKE3=1 nimble test # + testy specyficzne dla BLAKE3
nim c -d:fasttreeNoBlake3 -o:bin/fasttree src/fasttreecli.nim # fallback SHA-256
nim c -o:bin/fasttree src/fasttreecli.nim # prawdziwy BLAKE3
nim c -d:fasttreeIoUring -o:bin/fasttree src/fasttreecli.nim # + io_uring
nim c --app:staticlib --noMain -d:fasttreeNoBlake3 \
--nimcache:build/nimcache -o:build/libfasttree.a src/fasttree/capi.nim # dla Rust/C# BLAKE3 (real, nie fallback):
git clone --depth 1 https://github.com/BLAKE3-team/BLAKE3.git && cd BLAKE3/c
gcc -O2 -c blake3.c blake3_dispatch.c blake3_portable.c \
blake3_sse2_x86-64_unix.S blake3_sse41_x86-64_unix.S \
blake3_avx2_x86-64_unix.S blake3_avx512_x86-64_unix.S
ar rcs libblake3.a *.o && cp libblake3.a /usr/local/lib/ && cp blake3.h /usr/local/include/
# io_uring backend:
apt install liburing-dev
# composefs (buildImage/mountImage/deploy) — brak pakietu apt, budowa z źródeł:
apt install meson ninja-build libfuse3-dev pkg-config libssl-dev
git clone --depth 1 https://github.com/composefs/composefs.git && cd composefs
meson setup build -Dfuse=disabled -Dman=disabled && ninja -C build
cp build/tools/{mkcomposefs,composefs-info,mount.composefs} /usr/local/bin/
cp build/libcomposefs/libcomposefs.so.1.4.0 /usr/local/lib/
ln -sf libcomposefs.so.1.4.0 /usr/local/lib/libcomposefs.so.1 && ldconfig
# dm-verity:
apt install cryptsetup-bin # dostarcza veritysetup# Wymaga kompilatora `nim` w PATH — fasttree-sys/build.rs wywołuje go.
cargo build --workspace
cargo test --workspace
cargo build --workspace --features fasttree-sys/blake3 # z prawdziwym BLAKE3
# Cross-compilation (zweryfikowane dla aarch64-unknown-linux-gnu na hoście
# x86_64-linux; wymaga rust-std dla celu — np. `rustup target add` —
# i cross-gcc, np. `apt install gcc-aarch64-linux-gnu`):
cargo build -p fasttree-sys --target aarch64-unknown-linux-gnu
# Jeśli cross-gcc ma nietypową nazwę, ustaw:
FASTTREE_NIM_CROSS_CC=aarch64-linux-gnu-gcc cargo build --target aarch64-unknown-linux-gnuuse fasttree::{Store, Manifest};
let store = Store::open("/var/lib/fasttree/store")?;
// Store/Manifest niskopoziomowo:
let hash = store.put(b"dane")?;
let manifest = Manifest::build("/path/do/rootfs", &store)?;
// Pełny cykl pull+deploy bez binarki CLI:
let manifest = Manifest::pull("ghcr.io/org/repo:tag", "/var/cache/fasttree", "/tmp/work", &store)?;
let deploy = manifest.deploy_image(&store, "/tmp/materialized", "/var/lib/fasttree/image.cfs")?;
println!("digest={} verity_root={}", deploy.image_digest, deploy.verity_root_hash);.github/workflows/ci.yml: cztery joby — nim-test (nimble test, fallback
SHA-256), nim-test-blake3 (z realnym BLAKE3 zbudowanym z źródeł),
rust-test (cargo test --workspace), full-integration (composefs +
io_uring + dm-verity + CLI end-to-end, continue-on-error: true — wymaga
mount/device-mapper, których dostępność na hostowanych runnerach nie dało
się potwierdzić z tego środowiska).
Zrobione w tej iteracji: OCI Bearer auth, composefs (requireTool + digest), OverlayFS layering, dm-verity (format/verify/open), hardlinki+xattrs w layers.nim, io_uring readBatch, C API/Rust dla pull+deploy, cross-compilation w build.rs, CI.
Zrobione w kolejnej iteracji (patrz "Poprawki w tej iteracji" niżej):
status --diff, overlay list/overlay diff, preflight-checki dla
dm-verity/fs-verity + testy jednostkowe, rozszerzenie CI o realne próby
dmVerityOpen/mountVerified, dwie poprawki bugów w ci.yml.
Zostało:
- Uruchomienie
dmVerityOpen/mountVerified(nowe kroki CI) na prawdziwym GitHub Actions — lokalnie potwierdzone, że kod poprawnie działa/pomija się w zależności od możliwości jądra, ale sam runner GH Actions jest niepotwierdzony. - Uruchomienie
.github/workflows/ci.ymlna prawdziwym GitHub Actions i poprawki wynikające z realnego przebiegu (poza dwoma już znalezionymi i naprawionymi bugami — patrz niżej). - Pełny test cross-compilation (
cargo build --target=...) na maszynie z zainstalowanymrust-stddla celu — offline w tym środowisku (brak dostępu dostatic.rust-lang.orgw konfiguracji sieciowej sandboksu).
Wszystkie poniższe zweryfikowane empirycznie (skompilowane i uruchomione,
nie tylko nim check) w środowisku z zainstalowanym nim/nimble z apt:
cli.cmdStatus --diff— był stubem (echo "TODO"). Podpięty pod istniejącymanifest.diff(); dodano śledzeniePREVIOUS_TAGwcmdDeploy. Przetestowane na dwóch manifestach — poprawnie pokazuje added/removed/modified z liczbą chunków.fasttree overlay list/overlay diff <nazwa>— nowe komendy (overlay.listActiveOverlays, wpięcie istniejącegooverlay.listChangespod CLI). Przetestowane realnym montowaniem overlayfs (trwały + ulotny) — poprawne rozróżnienie zamontowany/niezamontowany po ręcznymumount.ioengine.nim— usunięty nieaktualny komentarz odsyłający do rozwiązanego już TODO wcomposefs.nim.composefs.deviceMapperAvailable()/fsVerityKernelSupport()— nowe preflight-checki przeddmVerityOpen/mountVerified, dające czytelny błąd od razu zamiast czekać na cryptyczny komunikat zveritysetup/mount.composefs. Pokryte testem jednostkowym (tests/test_composefs_capability.nim, wchodzi w składnimble test).- Dwa niezależne, wcześniej nieznane bugi w
ci.yml, znalezione przez faktyczne uruchomienie kroków lokalnie: (1) wzorzecnim c -r /dev/stdin <<'NIM'nie działa w tej wersji Nim (Error: cannot open '/dev/stdin.nim') — zamieniony na zapis do pliku tymczasowego; (2) treść heredoc dziedziczyła wcięcie YAML-a, co dawałoError: invalid indentationna poziomie modułu Nim — naprawione przezsedusuwający wspólny prefiks przed kompilacją. Oba potwierdzone jako realny problem I jako naprawione: zbudowanocomposefsz źródeł w tym środowisku i pełny testbuildImage+dmVerityFormat/dmVerityVerifyprzeszedł po poprawce. - Błędna nazwa pakietu w nowym kroku CI —
fsverity-utilsnie istnieje w Ubuntu; poprawna nazwa tofsverity(potwierdzoneapt-cache search). nimble tags— poletagsNIE istnieje w składni.nimble(próba dodania go rzucaError: undeclared identifier: 'tags', potwierdzone uruchomieniemnimble test). Tagi należą do wpisu w rejestrzenim-lang/packages(packages.json), nie do samego pakietu — patrzpackaging/README.mdipackaging/nimble-packages-entry.json.fasttree-sys/fasttree-rs(Cargo) — build był całkowicie zepsuty. Zgłoszone przez użytkownika po realnym uruchomieniucargo build --releasena maszynie z Nim 2.2.10. Kolejne łatanie objawów (zmiana[lib] namena wersję z podkreślnikiem) prowadziło tylko do kolejnych błędów (unresolved import,no matching package found) — bo prawdziwą przyczyną byłocrate-type = ["staticlib"]na obu crate'ach. Staticlib nie generuje.rlib, więcfasttree-rsfizycznie nie mogło zrobić zwykłegouse fasttree_sys. Żaden z tych crate'ów nie eksportuje własnegoextern "C" fn— jedyny prawdziwy plik.aw tym układzie tolibfasttree.askompilowany z Nima przezbuild.rs, i tak już linkowany bezpośrednio (cargo:rustc-link-lib=static=fasttree), całkowicie niezależnie odcrate-typesamych crate'ów Rust. Naprawa: usunięcie całej sekcji[lib]zfasttree-sys/Cargo.tomlifasttree-rs/Cargo.toml(domyślnyrlibw zupełności wystarcza), bez ruszania[package] name(który zgodnie z konwencją Cargo może i powinien mieć myślnik — to wyłącznie nazwa TARGETU biblioteki musi być poprawnym identyfikatorem Rusta). Zweryfikowane end-to-end:cargo build --release+cargo test --release(8/8 testów, w tym prawdziwy roundtrip store put/get przez FFI do skompilowanego Nima) przechodzą od razu, bez dalszych poprawek.