14 KiB
CLAUDE.md
Context for Claude Code sessions on this repo (libglacier-ng, the package manager for the Everest/Glacier Linux distribution). This file exists because there's no built-in way to hand off a claude.ai planning conversation into Claude Code — this is that handoff, written down.
For the symlink-routing / package-scope design specifically, see
PACKAGE_SCOPES.txt at the repo root — that doc is the source of truth
for that topic and isn't duplicated here.
What this project is
A from-scratch Linux distribution (Everest, built on the Glacier
package-management model) with its own package manager
(libglacier-ng + gpkg/gstore CLI tools) and its own build
tooling (gbuild/gworld) for bootstrapping a set of packages up to
a minimal bootable system.
Core architecture
gl_context_t (transaction.h) is the central abstraction: uid,
root_path, index_path, store_path, links_path, mode
(GL_CTX_LIVE / GL_CTX_STAGE), scope (GL_SCOPE_USR /
GL_SCOPE_SYS), lock_fd.
Transaction lifecycle: gl_init_live_context / gl_init_stage_context
→ gl_commit_transaction or gl_abort_transaction. Staged installs
hardlink-seed index/store from live at stage-init time (copy-on-write,
so removing a live package inside a transaction doesn't require special
casing). The links tree is deliberately not seeded — see below.
Commit does atomic rename-based promotion (stage → live, with the
previous live moved to old/ as a rollback point), fsyncs the parent
dirs, then calls gl_relink_store to regenerate symlinks and
gl_rebuild_index to regenerate the index — both from a fresh
GL_CTX_LIVE context, not the just-committed stage context.
Scopes:
GL_SCOPE_USR: per-uid,/glacier/usr/{index,store,links}/<uid>.GL_SCOPE_SYS: system-wide,/glacier/sys/{index,store}. Symlink destination depends on the package's repo — seePACKAGE_SCOPES.txt. Short version:repo == "base"→/usr; anything else →/glacier/sys/links. This is decided per-package (ingl_link_pkg/gl_relink_store), not baked into the context.
Repos: base (minimal working system, boot-critical by definition —
this is a repo-level classification, not a per-package flag), extra
(important but not required), community (everything else). extra
and community currently behave identically — see open items.
Safety mechanisms (read before touching links/symlink code)
ctx->links_path is not always glacier-exclusive territory —
for system-scope base packages it's literally /usr, which holds
plenty of content glacier has no business touching. Everything that
reads or writes a links destination has to treat it as "possibly
shared":
is_glacier_symlink(path, store_prefix)(istoreutils.c) is the one source of truth for "did glacier create this." True only ifpathis a symlink whose target lives understore_prefix. Never touch a path this returns false for.gl_link_pkgwill not overwrite an existing path unlessis_glacier_symlinksays it's safe to replace.gl_relink_storeprunes stale symlinks (prune_stale_links) rather than wiping and rebuilding the whole links directory — a fullrm -rfofctx->links_pathwould be catastrophic once that path can be/usr. It prunes both possible system-scope destinations (/glacier/sys/linksand/usr) since a single pass can't know in advance whether any base-repo packages are involved.- The stage links tree is never hardlink-seeded — it's genuinely
unused (
gl_link_pkgonly ever runs againstGL_CTX_LIVE;gl_relink_storealways builds its own fresh live context). Seeding it would mean hardlinking all of/usron every staged system transaction, and riskingEXDEVif/usrand the stage area are on different filesystems.
Bug history (so it doesn't get relitigated or reintroduced)
Roughly chronological, all already fixed in the current tree:
gl_abort_transactionnever actually released the lock file — a copy-paste bug wrote into the wrong variable. Fixed by moving offO_EXCL-based locking entirely:gl_begin_transactionnow justopen(O_CREAT)+flock(), and lock files are neverunlink()'d (the kernel releasesflock()automatically on process death, evenSIGKILL; explicit unlinking has its own TOCTOU race with a second process creating a new inode at the same path).EXDEVwasn't checked before renaming —gl_commit_transactionnowstat()s both sides and fails cleanly withGL_TXN_ERR_XDEVbefore attempting any renames if stage and live aren't on the same filesystem.- Segfault on install, root cause: the Makefile's
transactiontarget didn't linklibglacier_transaction.soagainstlibglacier_istoreutils.so/libglacier_log.so, even thoughtransaction.ccallsgl_relink_store()andlg_printf(). Fixed by adding those-lflags and an explicittransaction: log istoreutilsprerequisite (build order matters now). - Symlinks going stale after commit —
gl_link_pkgused to run during staged installs too, embedding the stage path as the symlink target. Fixed:gl_link_pkgonly runs forGL_CTX_LIVE; links are fully regenerated post-commit bygl_relink_store. - Dangling symlinks never cleaned up on removal — see "Safety
mechanisms" above; this is what
prune_stale_linksfixes. GL_TXN_ERR_RENAMEon every commit after the first — leftoverold/directory from a previous commit made the next commit'srename()fail withENOTEMPTY. Fixed:gl_commit_transactionwipesold_baseif present before using it.-Wstringop-truncationerrors (real bug, not noise) — fourstrncpy(dst, ctx->foo_path, PATH_MAX - 1)calls that could leavedstunterminated. Fixed by switching tosnprintf(dst, sizeof(dst), "%s", ...), which always terminates. (-Wformat-truncation, separately, is suppressed via-Wno-error=format-truncationinconfig.mk— those warnings are about genuinely-safesnprintftruncation, not a real bug.)gbuildbuild-system autodetection checked for a bareMakefilebeforeconfigure/configure.ac. Broke musl (and any project with a hand-rolled, non-autotools build system that ships both) since itsMakefileis non-functional withoutconfiguregeneratingconfig.makfirst. Fixed by reordering the checks.- Index only stored
repo::pkgname, no version. Nowrepo::pkgname::version(gl_rebuild_indexlooks up the<pkgname>-<version>store subdirectory).struct gpkg_entryinistoreutils.hneeds achar *ver;field — this was a manual header edit, double check it's actually present. gworldrecipe parser silently droppedSYS_PROFILE— the key-matching regex only matched uppercase letters, not underscores. Fixed to^([%u_][%u%d_]*)=(.*)$. Caught by an actual precedence test, not code review — worth remembering that class of bug (a field silently never parsing, falling back to a default that happens to look right) is easy to miss by inspection alone.
CLI additions
gpkg -s/--system: operate againstGL_SCOPE_SYSinstead of a uid's tree. Requires root (checked viageteuid()). Must appear before-l/-xin a grouped flag string (e.g.-sl, not-ls) — flags are handled in parse order, same constraint-V/-Salready had.gstore -N/--new-system: callsgl_init_sys(). Requires root.gbuild -P/--profile PROFILE: system profile,ARCH-LIBC[-FEATURE...](e.g.x86_64-musl,x86_64-glibc-multilib). Derives a cross-compile prefix (translatingglibc→gnuper real GNU triplet convention), detects native-vs-cross against the build host, and only pre-fills-t/-xdefaults for whichever the user didn't already set explicitly. Features export asGL_FEATURE_<NAME>=1env vars for custom-b/-icommands to branch on — gbuild itself doesn't hardcode per-feature behavior, that's intentional.
Build system
Makefile builds separate .sos per module
(libglacier_log.so, libglacier_pkg.so, libglacier_istoreutils.so,
libglacier_transaction.so, libglacier_dag.so) — this split is
deliberate Unix-philosophy design, not an accident: a third-party
glacier-compatible program should be able to link only
libglacier_log.so without pulling in everything else. gpkg/gstore
load the specific per-module .sos they need directly, matching this.
There's also a libglacier-ng Makefile target producing a combined
.so — this exists solely for the Lua FFI test harness's
convenience (one thing to ffi.load() instead of juggling rpath
chains in a throwaway test process). It is not in all and not
installed. Don't reach for it outside tests/ — that's a sign
something should be split-loaded instead.
config.mk holds CFLAGS, including -Wno-error=format-truncation
(see bug #7 above for why that specific warning is suppressed and
others aren't).
Lua tooling
gpkg,gstore: LuaJIT + FFI, calling directly into the.sos.lib/glacier_cdef.lua: single shared source of truth for everyffi.cdef()declaration, used bygpkg,gstore, and the test suite. This used to be duplicated three ways, which is exactly what let a stale signature drift silently intogpkgat one point — if you're editing a C function signature, this file needs the matching update, and it's the only place that needs it now.tests/*.lua: LuaJIT FFI test suite exercising real filesystem/process behavior (fork+SIGKILLfor lock crash-recovery, realmount()for theEXDEVcheck, etc.) — not mocked. Run via:cd tests make -C .. libglacier-ng # builds the test-only combined .so GLACIER_TEST_CONFIRM=yes LIBGLACIER_SO=$(pwd)/../build/lib/shared/libglacier-ng.so luajit run_all.luatest_exdev.luaneeds--cap-add=SYS_ADMINin Docker to actually exercise the mount path; it skips cleanly (not a failure) without it.
Build automation (bootstrap toolchain)
gbuild(bash): single-package builder. Autodetects build system (cmake/meson/cargo/go/autotools/make), supports cross-compilation (-t/-x, now also-Pfor profiles).gworld(Lua): batch orchestrator. Readsrecipes/*.recipe, topologically sorts byDEPS, invokesgbuildonce per package in order.-s SYSROOTgives every package in the batch a shared staging dir — relies ongbuild -pnever wiping its target, so headers/libs accumulate across the batch for free.- Recipe format:
NAME VER REPO URL/REF-or-SRC DEPS BUILD INSTALL FLAGS SYS_PROFILE(plainkey=value, seerecipes/musl.recipefor a real example). - Real multi-pass toolchain bootstraps (LFS-style — gcc built twice,
etc.) should use separate recipes per pass (
gcc-pass1,gcc-pass2), publishing only the final pass under the real name/repo. The DAG models build steps, not abstract packages. grootstrapneeds a rebuild, not yet done. The old design (rawtarextraction into a target dir + chroot + register everything under an arbitrary bootstrap uid) predates theGL_SCOPE_SYS+ base-repo design and is now more complicated than necessary. Oncegpkg/gstorethemselves exist on the target (still an unsolved bootstrapping-the-bootstrapper problem — how do they get there before a working package manager exists to install them with), every package includingbaseones can go through the samegpkg -s -lpath; the repo alone routes symlinks correctly. No more special-casing base packages during bootstrap.
Known open items / stubs
gpkg -f/-u(merge/update) are unimplemented stubs.-freferences an undefineduidvariable (should beuidn) — will nil-concatenation-error the moment it's actually implemented.gpkg -x's confirmation summary shows placeholderunknown::pkg 0.0.0 (unknown)since it has no local manifest to read version/repo from for a bare package name. Would needresolve_repocalled before building the summary, not just before the removal itself.gpkg -xdoesn't print a completion message on success (-ldoes: "Completed with no errors."). Cosmetic inconsistency.- Reinstalling an up-to-date package double-logs ("Installed X" + "staged from X" for the same event).
extravscommunityrepos don't currently diverge in behavior — both just mean "not base" for symlink-routing purposes.gl_relink_store/gl_rebuild_indexare full rescans of the store on every commit — fine at current scale, worth revisiting if large transactions become common (a dirty-set of touched packages instead of full rescans).- Package manifests don't record which
SYS_PROFILEthey were built under — deliberately not added yet (would need apkg.c/gl_gpm2gpkgchange), flagged as a future option, not a gap that needs fixing now. - A stray nested
lib/glacier/lib/glacier/directory was spotted once in atreelisting — looked like a leftovermake installartifact, not in any load path, harmless but worth arm -rfeventually.
Conventions worth knowing before editing
- C source generally uses tabs for indentation, though the codebase is inconsistent about it in places (some blocks use spaces). ALWAYS USE TABS WHEN POSSIBLE.
PATH_MAX-sized stack buffers +snprintfis the standard pattern for path construction throughout; prefer it overstrncpyfor anything copying into a fixed buffer (see bug #7).- Don't add convenience wrappers that silently touch a broader
filesystem scope than what's asked — this codebase has already been
bitten twice by exactly that class of mistake (
gl_relink_store's wipe,gl_link_pkg's blindunlink), both only becoming dangerous once a path assumption (links_pathis glacier-exclusive) quietly stopped holding.