Files
libglacier-ng/CLAUDE.md
2026-07-19 17:49:23 -04:00

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_contextgl_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 — see PACKAGE_SCOPES.txt. Short version: repo == "base"/usr; anything else → /glacier/sys/links. This is decided per-package (in gl_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.

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 if path is a symlink whose target lives under store_prefix. Never touch a path this returns false for.
  • gl_link_pkg will not overwrite an existing path unless is_glacier_symlink says it's safe to replace.
  • gl_relink_store prunes stale symlinks (prune_stale_links) rather than wiping and rebuilding the whole links directory — a full rm -rf of ctx->links_path would be catastrophic once that path can be /usr. It prunes both possible system-scope destinations (/glacier/sys/links and /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_pkg only ever runs against GL_CTX_LIVE; gl_relink_store always builds its own fresh live context). Seeding it would mean hardlinking all of /usr on every staged system transaction, and risking EXDEV if /usr and 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:

  1. gl_abort_transaction never actually released the lock file — a copy-paste bug wrote into the wrong variable. Fixed by moving off O_EXCL-based locking entirely: gl_begin_transaction now just open(O_CREAT) + flock(), and lock files are never unlink()'d (the kernel releases flock() automatically on process death, even SIGKILL; explicit unlinking has its own TOCTOU race with a second process creating a new inode at the same path).
  2. EXDEV wasn't checked before renaminggl_commit_transaction now stat()s both sides and fails cleanly with GL_TXN_ERR_XDEV before attempting any renames if stage and live aren't on the same filesystem.
  3. Segfault on install, root cause: the Makefile's transaction target didn't link libglacier_transaction.so against libglacier_istoreutils.so/libglacier_log.so, even though transaction.c calls gl_relink_store() and lg_printf(). Fixed by adding those -l flags and an explicit transaction: log istoreutils prerequisite (build order matters now).
  4. Symlinks going stale after commitgl_link_pkg used to run during staged installs too, embedding the stage path as the symlink target. Fixed: gl_link_pkg only runs for GL_CTX_LIVE; links are fully regenerated post-commit by gl_relink_store.
  5. Dangling symlinks never cleaned up on removal — see "Safety mechanisms" above; this is what prune_stale_links fixes.
  6. GL_TXN_ERR_RENAME on every commit after the first — leftover old/ directory from a previous commit made the next commit's rename() fail with ENOTEMPTY. Fixed: gl_commit_transaction wipes old_base if present before using it.
  7. -Wstringop-truncation errors (real bug, not noise) — four strncpy(dst, ctx->foo_path, PATH_MAX - 1) calls that could leave dst unterminated. Fixed by switching to snprintf(dst, sizeof(dst), "%s", ...), which always terminates. (-Wformat-truncation, separately, is suppressed via -Wno-error=format-truncation in config.mk — those warnings are about genuinely-safe snprintf truncation, not a real bug.)
  8. gbuild build-system autodetection checked for a bare Makefile before configure/configure.ac. Broke musl (and any project with a hand-rolled, non-autotools build system that ships both) since its Makefile is non-functional without configure generating config.mak first. Fixed by reordering the checks.
  9. Index only stored repo::pkgname, no version. Now repo::pkgname::version (gl_rebuild_index looks up the <pkgname>-<version> store subdirectory). struct gpkg_entry in istoreutils.h needs a char *ver; field — this was a manual header edit, double check it's actually present.
  10. gworld recipe parser silently dropped SYS_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 against GL_SCOPE_SYS instead of a uid's tree. Requires root (checked via geteuid()). Must appear before -l/-x in a grouped flag string (e.g. -sl, not -ls) — flags are handled in parse order, same constraint -V/-S already had.
  • gstore -N / --new-system: calls gl_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 (translating glibcgnu per real GNU triplet convention), detects native-vs-cross against the build host, and only pre-fills -t/-x defaults for whichever the user didn't already set explicitly. Features export as GL_FEATURE_<NAME>=1 env vars for custom -b/-i commands 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 every ffi.cdef() declaration, used by gpkg, gstore, and the test suite. This used to be duplicated three ways, which is exactly what let a stale signature drift silently into gpkg at 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+SIGKILL for lock crash-recovery, real mount() for the EXDEV check, 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.lua
    
    test_exdev.lua needs --cap-add=SYS_ADMIN in 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 -P for profiles).
  • gworld (Lua): batch orchestrator. Reads recipes/*.recipe, topologically sorts by DEPS, invokes gbuild once per package in order. -s SYSROOT gives every package in the batch a shared staging dir — relies on gbuild -p never 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 (plain key=value, see recipes/musl.recipe for 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.
  • grootstrap needs a rebuild, not yet done. The old design (raw tar extraction into a target dir + chroot + register everything under an arbitrary bootstrap uid) predates the GL_SCOPE_SYS + base-repo design and is now more complicated than necessary. Once gpkg/gstore themselves 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 including base ones can go through the same gpkg -s -l path; 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. -f references an undefined uid variable (should be uidn) — will nil-concatenation-error the moment it's actually implemented.
  • gpkg -x's confirmation summary shows placeholder unknown::pkg 0.0.0 (unknown) since it has no local manifest to read version/repo from for a bare package name. Would need resolve_repo called before building the summary, not just before the removal itself.
  • gpkg -x doesn't print a completion message on success (-l does: "Completed with no errors."). Cosmetic inconsistency.
  • Reinstalling an up-to-date package double-logs ("Installed X" + "staged from X" for the same event).
  • extra vs community repos don't currently diverge in behavior — both just mean "not base" for symlink-routing purposes.
  • gl_relink_store/gl_rebuild_index are 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_PROFILE they were built under — deliberately not added yet (would need a pkg.c / gl_gpm2gpkg change), flagged as a future option, not a gap that needs fixing now.
  • A stray nested lib/glacier/lib/glacier/ directory was spotted once in a tree listing — looked like a leftover make install artifact, not in any load path, harmless but worth a rm -rf eventually.

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 + snprintf is the standard pattern for path construction throughout; prefer it over strncpy for 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 blind unlink), both only becoming dangerous once a path assumption (links_path is glacier-exclusive) quietly stopped holding.